akanjs 2.3.11-rc.9 → 2.3.12-rc.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 +21 -0
- package/client/csrTypes.ts +2 -0
- package/common/routeConvention.ts +8 -5
- package/fetch/client/fetchClient.ts +29 -14
- package/fetch/serializer/fetch.serializer.ts +10 -0
- package/package.json +1 -1
- package/server/resolver/signal.resolver.ts +3 -3
- package/server/routeTreeBuilder.ts +37 -4
- package/signal/serializer/fetch.serializer.ts +10 -0
- package/signal/signalContext.ts +5 -0
- package/signal/slice.ts +17 -15
- package/signal/types.ts +3 -0
- package/types/client/csrTypes.d.ts +2 -0
- package/types/common/routeConvention.d.ts +1 -1
- package/types/signal/signalContext.d.ts +1 -0
- package/types/signal/slice.d.ts +6 -0
- package/types/signal/types.d.ts +3 -0
- package/types/ui/Button.d.ts +6 -1
- package/types/ui/DatePicker.d.ts +12 -9
- package/types/ui/Dropdown.d.ts +6 -1
- package/types/ui/Empty.d.ts +7 -1
- package/types/ui/Input.d.ts +12 -8
- package/types/ui/Loading/index.d.ts +11 -6
- package/types/ui/Menu.d.ts +9 -4
- package/types/ui/Modal.d.ts +11 -1
- package/types/ui/Pagination.d.ts +6 -1
- package/types/ui/Popconfirm.d.ts +7 -1
- package/types/ui/Radio.d.ts +7 -4
- package/types/ui/Select.d.ts +6 -1
- package/types/ui/Table.d.ts +6 -1
- package/types/ui/ToggleSelect.d.ts +11 -7
- package/types/ui/UiOverride/Provider.d.ts +14 -0
- package/types/ui/UiOverride/context.d.ts +72 -0
- package/types/ui/UiOverride/createOverridable.d.ts +9 -0
- package/types/ui/UiOverride/index.d.ts +5 -0
- package/types/ui/UiOverride/override.d.ts +18 -0
- package/types/ui/UiOverride/useUiOverride.d.ts +7 -0
- package/types/ui/UiOverride.d.ts +1 -0
- package/types/ui/Unauthorized.d.ts +8 -3
- package/types/ui/index.d.ts +1 -0
- package/types/webkit/usePushNotification.d.ts +10 -1
- package/ui/Button.tsx +15 -2
- package/ui/DatePicker.tsx +19 -9
- package/ui/Dropdown.tsx +9 -1
- package/ui/Empty.tsx +11 -1
- package/ui/Input.tsx +22 -14
- package/ui/Loading/index.tsx +15 -1
- package/ui/Menu.tsx +12 -3
- package/ui/Modal.tsx +13 -1
- package/ui/Pagination.tsx +9 -1
- package/ui/Popconfirm.tsx +9 -1
- package/ui/Radio.tsx +14 -3
- package/ui/Select.tsx +22 -2
- package/ui/Table.tsx +8 -1
- package/ui/ToggleSelect.tsx +31 -5
- package/ui/UiOverride/Provider.tsx +22 -0
- package/ui/UiOverride/context.ts +85 -0
- package/ui/UiOverride/createOverridable.tsx +25 -0
- package/ui/UiOverride/index.ts +5 -0
- package/ui/UiOverride/override.ts +19 -0
- package/ui/UiOverride/useUiOverride.ts +15 -0
- package/ui/Unauthorized.tsx +12 -3
- package/ui/index.ts +10 -0
- package/webkit/bootCsr.tsx +28 -5
- package/webkit/usePushNotification.tsx +21 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# akanjs
|
|
2
2
|
|
|
3
|
+
## 2.3.11
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 595390a: feat: UiOverride 시스템 및 \_overrides.tsx 지원 추가
|
|
8
|
+
|
|
9
|
+
- `akanjs/ui/UiOverride` 추가: `Provider`, `createOverridable`, `useUiOverride`, `override` API로 UI 컴포넌트 커스터마이징 지원
|
|
10
|
+
- 모든 akanjs UI 컴포넌트(Button, Modal, Select, Table 등)에 `useUiOverride()` 통합
|
|
11
|
+
- 라우트 시스템에 `_overrides.tsx` 지원 추가 (routeConvention, routeTreeBuilder)
|
|
12
|
+
- qualityScanner에 `_overrides.tsx` 파일 검증 로직 추가
|
|
13
|
+
- 앱 예제: `apps/minimal`에 `_overrides.tsx`, `BrandModal`, `OverrideDemo` 추가
|
|
14
|
+
- `apps/akan` 문서에 UI 커스터마이징 가이드 페이지 추가
|
|
15
|
+
- devkit에 `no-throw-raw-error.grit` lint rule 추가
|
|
16
|
+
- `PushNotificationServer.ts` 리팩토링
|
|
17
|
+
- biome.json 업데이트 및 패키지 의존성 정리
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 5ce752a: enhance: add host option for staging server tests
|
|
22
|
+
- 5ce752a: add host option for staging server tests
|
|
23
|
+
|
|
3
24
|
## 2.3.10
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/client/csrTypes.ts
CHANGED
|
@@ -191,6 +191,8 @@ export interface Route {
|
|
|
191
191
|
path: string;
|
|
192
192
|
renderPage?: RouteRender;
|
|
193
193
|
renderLayout?: RouteRender;
|
|
194
|
+
/** Synthetic layout render from a `_overrides.tsx` at this node; wraps the subtree in a UI-override provider. */
|
|
195
|
+
renderOverrides?: RouteRender;
|
|
194
196
|
pageIncludesOwnLayout?: boolean;
|
|
195
197
|
isSpecialRoute?: boolean;
|
|
196
198
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const ROUTE_SOURCE_RE = /^\.\/(.+)\.(tsx|ts|jsx|js)$/;
|
|
2
2
|
const SOURCE_EXT_RE = /\.(tsx|ts|jsx|js)$/;
|
|
3
|
-
const RESERVED_ROUTE_FILES = new Set(["_layout", "_index"]);
|
|
3
|
+
const RESERVED_ROUTE_FILES = new Set(["_layout", "_index", "_overrides"]);
|
|
4
4
|
const INTERNAL_ROOT_LAYOUT_LEAF = "__root_layout";
|
|
5
5
|
const IMPLICIT_LOCALE_SEGMENT = "[lang]";
|
|
6
6
|
const SPECIAL_ROUTE_LEAVES = new Set(["robots.txt"]);
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const DIRECTORY_SCOPED_LEAVES = new Set(["_layout", "_index", "_overrides"]);
|
|
9
|
+
|
|
10
|
+
export type RouteModuleKind = "page" | "layout" | "overrides";
|
|
9
11
|
|
|
10
12
|
export interface ParsedRouteModuleKey {
|
|
11
13
|
key: string;
|
|
@@ -51,7 +53,7 @@ export function validatePageSourceFile(filePath: string, options: ValidatePageSo
|
|
|
51
53
|
if (ext !== "tsx") throw new Error(`[route-convention] route source files under page/ must use .tsx: ${displayPath}`);
|
|
52
54
|
if (leaf.startsWith("_") && !RESERVED_ROUTE_FILES.has(leaf) && leaf !== INTERNAL_ROOT_LAYOUT_LEAF)
|
|
53
55
|
throw new Error(
|
|
54
|
-
`[route-convention] only _index.tsx and
|
|
56
|
+
`[route-convention] only _index.tsx, _layout.tsx and _overrides.tsx are allowed as reserved route files under page/: ${displayPath}`,
|
|
55
57
|
);
|
|
56
58
|
if (/^[A-Z]/.test(leaf))
|
|
57
59
|
throw new Error(`[route-convention] route page filenames must not start with an uppercase letter: ${displayPath}`);
|
|
@@ -97,13 +99,14 @@ export function parseRouteModuleKey(key: string): ParsedRouteModuleKey {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
const isInternalRootLayout = leaf === INTERNAL_ROOT_LAYOUT_LEAF;
|
|
100
|
-
const kind: RouteModuleKind =
|
|
102
|
+
const kind: RouteModuleKind =
|
|
103
|
+
leaf === "_layout" || isInternalRootLayout ? "layout" : leaf === "_overrides" ? "overrides" : "page";
|
|
101
104
|
if (leaf.startsWith("_") && !RESERVED_ROUTE_FILES.has(leaf) && !isInternalRootLayout) {
|
|
102
105
|
throw new Error(`[route-convention] unsupported reserved route file "${leaf}" in ${key}`);
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
const sourceRouteSegments =
|
|
106
|
-
leaf
|
|
109
|
+
DIRECTORY_SCOPED_LEAVES.has(leaf) || isInternalRootLayout ? moduleSegments.slice(0, -1) : moduleSegments;
|
|
107
110
|
const isSpecialRoute = kind === "page" && SPECIAL_ROUTE_LEAVES.has(leaf);
|
|
108
111
|
const routeSegments = isSpecialRoute ? sourceRouteSegments : [IMPLICIT_LOCALE_SEGMENT, ...sourceRouteSegments];
|
|
109
112
|
for (const segment of routeSegments) validateRouteSegment(segment, key);
|
|
@@ -101,6 +101,9 @@ export class FetchClient {
|
|
|
101
101
|
: undefined,
|
|
102
102
|
getGuards: signal.getGuards ?? current.getGuards,
|
|
103
103
|
cruGuards: signal.cruGuards ?? current.cruGuards,
|
|
104
|
+
createGuards: signal.createGuards ?? current.createGuards,
|
|
105
|
+
updateGuards: signal.updateGuards ?? current.updateGuards,
|
|
106
|
+
removeGuards: signal.removeGuards ?? current.removeGuards,
|
|
104
107
|
}
|
|
105
108
|
: signal;
|
|
106
109
|
}
|
|
@@ -353,6 +356,10 @@ export class FetchClient {
|
|
|
353
356
|
modelId: `${refName}Id`,
|
|
354
357
|
lightModel: `light${capRefName}`,
|
|
355
358
|
};
|
|
359
|
+
|
|
360
|
+
const createGuards = signal.createGuards ?? signal.cruGuards;
|
|
361
|
+
const updateGuards = signal.updateGuards ?? signal.cruGuards;
|
|
362
|
+
const removeGuards = signal.removeGuards ?? signal.cruGuards;
|
|
356
363
|
const endpoint: { [key: string]: SerializedEndpoint } = {};
|
|
357
364
|
if (signal.getGuards) {
|
|
358
365
|
endpoint[names.model] = {
|
|
@@ -368,13 +375,15 @@ export class FetchClient {
|
|
|
368
375
|
guards: signal.getGuards,
|
|
369
376
|
};
|
|
370
377
|
}
|
|
371
|
-
if (
|
|
378
|
+
if (createGuards) {
|
|
372
379
|
endpoint[names.createModel] = {
|
|
373
380
|
type: "mutation",
|
|
374
381
|
args: [{ type: "body", name: "data", refName, modelType: "input" }],
|
|
375
382
|
returns: { refName, modelType: "full" },
|
|
376
|
-
guards:
|
|
383
|
+
guards: createGuards,
|
|
377
384
|
};
|
|
385
|
+
}
|
|
386
|
+
if (updateGuards) {
|
|
378
387
|
endpoint[names.updateModel] = {
|
|
379
388
|
type: "mutation",
|
|
380
389
|
args: [
|
|
@@ -382,13 +391,15 @@ export class FetchClient {
|
|
|
382
391
|
{ type: "body", name: "data", refName, modelType: "input" },
|
|
383
392
|
],
|
|
384
393
|
returns: { refName, modelType: "full" },
|
|
385
|
-
guards:
|
|
394
|
+
guards: updateGuards,
|
|
386
395
|
};
|
|
396
|
+
}
|
|
397
|
+
if (removeGuards) {
|
|
387
398
|
endpoint[names.removeModel] = {
|
|
388
399
|
type: "mutation",
|
|
389
400
|
args: [{ type: "param", name: names.modelId, refName: "ID" }],
|
|
390
401
|
returns: { refName, modelType: "full" },
|
|
391
|
-
guards:
|
|
402
|
+
guards: removeGuards,
|
|
392
403
|
};
|
|
393
404
|
}
|
|
394
405
|
return endpoint;
|
|
@@ -414,7 +425,9 @@ export class FetchClient {
|
|
|
414
425
|
this.#setHandlerFactory(key, () => this.#makeHttpFn(key, value, signal.prefix));
|
|
415
426
|
});
|
|
416
427
|
|
|
417
|
-
|
|
428
|
+
const anyCruGuards = signal.cruGuards ?? signal.createGuards ?? signal.updateGuards ?? signal.removeGuards;
|
|
429
|
+
const updateGuards = signal.updateGuards ?? signal.cruGuards;
|
|
430
|
+
if (anyCruGuards) {
|
|
418
431
|
this.#setHandlerFactory(
|
|
419
432
|
names.viewModel,
|
|
420
433
|
() =>
|
|
@@ -461,15 +474,17 @@ export class FetchClient {
|
|
|
461
474
|
return { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() };
|
|
462
475
|
}) as FetchHandler,
|
|
463
476
|
);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
477
|
+
if (updateGuards) {
|
|
478
|
+
this.#setHandlerFactory(
|
|
479
|
+
names.mergeModel,
|
|
480
|
+
() =>
|
|
481
|
+
(async (modelOrId: string | { id: string }, data: UnknownRecord, option?: FetchPolicy) => {
|
|
482
|
+
const id = typeof modelOrId === "string" ? modelOrId : modelOrId.id;
|
|
483
|
+
const updateFn = this.#requireHandler(names.updateModel, names.mergeModel);
|
|
484
|
+
return await updateFn(id, data, option);
|
|
485
|
+
}) as FetchHandler,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
473
488
|
}
|
|
474
489
|
|
|
475
490
|
this.#setHandlerFactory(
|
|
@@ -102,6 +102,16 @@ export class FetchSerializer {
|
|
|
102
102
|
...(sliceCls.cruGuards.filter((g) => g.name !== "None").length
|
|
103
103
|
? { cruGuards: sliceCls.cruGuards.map((g) => g.name) }
|
|
104
104
|
: {}),
|
|
105
|
+
|
|
106
|
+
...(sliceCls.createGuards !== sliceCls.cruGuards && sliceCls.createGuards.filter((g) => g.name !== "None").length
|
|
107
|
+
? { createGuards: sliceCls.createGuards.map((g) => g.name) }
|
|
108
|
+
: {}),
|
|
109
|
+
...(sliceCls.updateGuards !== sliceCls.cruGuards && sliceCls.updateGuards.filter((g) => g.name !== "None").length
|
|
110
|
+
? { updateGuards: sliceCls.updateGuards.map((g) => g.name) }
|
|
111
|
+
: {}),
|
|
112
|
+
...(sliceCls.removeGuards !== sliceCls.cruGuards && sliceCls.removeGuards.filter((g) => g.name !== "None").length
|
|
113
|
+
? { removeGuards: sliceCls.removeGuards.map((g) => g.name) }
|
|
114
|
+
: {}),
|
|
105
115
|
endpoint,
|
|
106
116
|
};
|
|
107
117
|
}
|
package/package.json
CHANGED
|
@@ -232,14 +232,14 @@ export class SignalResolver {
|
|
|
232
232
|
});
|
|
233
233
|
|
|
234
234
|
endpointObj[`create${capitalizedRefName}`] = (builder as any)
|
|
235
|
-
.mutation(cnst.full, { guards: sliceCls.
|
|
235
|
+
.mutation(cnst.full, { guards: sliceCls.createGuards })
|
|
236
236
|
.body("data", cnst.input)
|
|
237
237
|
.exec(async function (this: any, data: any) {
|
|
238
238
|
return await this[serviceName].__create(data);
|
|
239
239
|
});
|
|
240
240
|
|
|
241
241
|
endpointObj[`update${capitalizedRefName}`] = (builder as any)
|
|
242
|
-
.mutation(cnst.full, { guards: sliceCls.
|
|
242
|
+
.mutation(cnst.full, { guards: sliceCls.updateGuards })
|
|
243
243
|
.param(`${refName}Id`, ID)
|
|
244
244
|
.body("data", cnst.input)
|
|
245
245
|
.exec(async function (this: any, id: string, data: any) {
|
|
@@ -247,7 +247,7 @@ export class SignalResolver {
|
|
|
247
247
|
});
|
|
248
248
|
|
|
249
249
|
endpointObj[`remove${capitalizedRefName}`] = (builder as any)
|
|
250
|
-
.mutation(cnst.full, { guards: sliceCls.
|
|
250
|
+
.mutation(cnst.full, { guards: sliceCls.removeGuards })
|
|
251
251
|
.param(`${refName}Id`, ID)
|
|
252
252
|
.exec(async function (this: any, id: string) {
|
|
253
253
|
return await this[serviceName].__remove(id);
|
|
@@ -18,9 +18,12 @@ import {
|
|
|
18
18
|
parseRouteModuleKey,
|
|
19
19
|
routeSegmentToTreePath,
|
|
20
20
|
} from "akanjs/common";
|
|
21
|
+
import { createElement } from "react";
|
|
21
22
|
import { validatePageConfig } from "../client/frameConfig";
|
|
22
23
|
import { resolveHeadExport, resolveMetadataHead } from "./metadata";
|
|
23
24
|
|
|
25
|
+
type RouteModuleKindWithOverrides = "page" | "layout" | "overrides";
|
|
26
|
+
|
|
24
27
|
export type PagesContext = Record<string, () => Promise<RouteModule>>;
|
|
25
28
|
|
|
26
29
|
export const defaultPageState: PageState = {
|
|
@@ -197,6 +200,14 @@ export class RouteTreeBuilder {
|
|
|
197
200
|
const targetPath = pathSegments[pathSegments.length - 1];
|
|
198
201
|
if (!targetPath) return;
|
|
199
202
|
|
|
203
|
+
if (parsed.kind === "overrides") {
|
|
204
|
+
targetRouteMap.set(targetPath, {
|
|
205
|
+
...(targetRouteMap.get(targetPath) ?? { path: targetPath, children: new Map<string, Route>() }),
|
|
206
|
+
renderOverrides: this.#makeOverridesRender(filePath, loader),
|
|
207
|
+
} as Route);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
200
211
|
const routeRender = RouteTreeBuilder.#makeRouteRender(filePath, parsed.kind, loader);
|
|
201
212
|
targetRouteMap.set(targetPath, {
|
|
202
213
|
...(targetRouteMap.get(targetPath) ?? { path: targetPath, children: new Map<string, Route>() }),
|
|
@@ -224,8 +235,10 @@ export class RouteTreeBuilder {
|
|
|
224
235
|
const pathSegments = [...parentPaths, ...(currentPathSegment ? [currentPathSegment] : [])];
|
|
225
236
|
const currentRootLayout = isRoot && route.renderLayout ? route.renderLayout : null;
|
|
226
237
|
const currentLayout = !isRoot && route.renderLayout ? route.renderLayout : null;
|
|
238
|
+
|
|
239
|
+
const currentOverrideRenders = route.renderOverrides ? [route.renderOverrides] : [];
|
|
227
240
|
const renderRootLayouts = [...parentRootLayouts, ...(currentRootLayout ? [currentRootLayout] : [])];
|
|
228
|
-
const renderLayouts = [...parentLayouts, ...(currentLayout ? [currentLayout] : [])];
|
|
241
|
+
const renderLayouts = [...parentLayouts, ...currentOverrideRenders, ...(currentLayout ? [currentLayout] : [])];
|
|
229
242
|
if (route.renderLayout) {
|
|
230
243
|
this.#fallbackRoutes.push({
|
|
231
244
|
path: routePath,
|
|
@@ -237,7 +250,10 @@ export class RouteTreeBuilder {
|
|
|
237
250
|
const routeHead = RouteTreeBuilder.#composeHeadResolvers(route.renderLayout?.resolveHead, parentHead);
|
|
238
251
|
const pageRenderRootLayouts =
|
|
239
252
|
route.pageIncludesOwnLayout === false && currentRootLayout ? parentRootLayouts : renderRootLayouts;
|
|
240
|
-
const pageRenderLayouts =
|
|
253
|
+
const pageRenderLayouts =
|
|
254
|
+
route.pageIncludesOwnLayout === false && currentLayout
|
|
255
|
+
? [...parentLayouts, ...currentOverrideRenders]
|
|
256
|
+
: renderLayouts;
|
|
241
257
|
const pageHead = route.pageIncludesOwnLayout === false ? parentHead : routeHead;
|
|
242
258
|
return [
|
|
243
259
|
...(route.renderPage
|
|
@@ -262,7 +278,7 @@ export class RouteTreeBuilder {
|
|
|
262
278
|
];
|
|
263
279
|
}
|
|
264
280
|
|
|
265
|
-
static #makeLazyModule(key: string, kind:
|
|
281
|
+
static #makeLazyModule(key: string, kind: RouteModuleKindWithOverrides, loader: () => Promise<RouteModule>) {
|
|
266
282
|
let cached: RouteModule | null = null;
|
|
267
283
|
let loaded = false;
|
|
268
284
|
RouteTreeBuilder.#moduleCacheStats.moduleCount += 1;
|
|
@@ -285,7 +301,12 @@ export class RouteTreeBuilder {
|
|
|
285
301
|
};
|
|
286
302
|
}
|
|
287
303
|
|
|
288
|
-
static #validateRouteModuleExports(key: string, kind:
|
|
304
|
+
static #validateRouteModuleExports(key: string, kind: RouteModuleKindWithOverrides, mod: RouteModule) {
|
|
305
|
+
if (kind === "overrides") {
|
|
306
|
+
|
|
307
|
+
if (!mod.default) throw new Error(`[route-convention] ${key} generated override wrapper has no default export`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
289
310
|
const parsed = parseRouteModuleKey(key);
|
|
290
311
|
const allowed =
|
|
291
312
|
kind === "page"
|
|
@@ -378,6 +399,18 @@ export class RouteTreeBuilder {
|
|
|
378
399
|
return routeRender;
|
|
379
400
|
}
|
|
380
401
|
|
|
402
|
+
#makeOverridesRender(key: string, loader: () => Promise<RouteModule>): RouteRender {
|
|
403
|
+
const loadModule = RouteTreeBuilder.#makeLazyModule(key, "overrides", loader);
|
|
404
|
+
return {
|
|
405
|
+
isAsync: true,
|
|
406
|
+
render: (async ({ children }: LayoutProps) => {
|
|
407
|
+
const mod = await loadModule();
|
|
408
|
+
if (!mod.default) throw new Error(`[route-convention] ${key} generated override wrapper has no default export`);
|
|
409
|
+
return createElement(mod.default as never, { children } as never);
|
|
410
|
+
}) as RouteRender["render"],
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
381
414
|
static #composeHeadResolvers(...resolvers: (ResolveHead | undefined)[]): ResolveHead | undefined {
|
|
382
415
|
const chain = resolvers.filter((resolver): resolver is ResolveHead => Boolean(resolver));
|
|
383
416
|
if (chain.length === 0) return undefined;
|
|
@@ -99,6 +99,16 @@ export class FetchSerializer {
|
|
|
99
99
|
...(sliceCls.cruGuards.filter((g) => g.name !== "None").length
|
|
100
100
|
? { cruGuards: sliceCls.cruGuards.map((g) => g.name) }
|
|
101
101
|
: {}),
|
|
102
|
+
|
|
103
|
+
...(sliceCls.createGuards !== sliceCls.cruGuards && sliceCls.createGuards.filter((g) => g.name !== "None").length
|
|
104
|
+
? { createGuards: sliceCls.createGuards.map((g) => g.name) }
|
|
105
|
+
: {}),
|
|
106
|
+
...(sliceCls.updateGuards !== sliceCls.cruGuards && sliceCls.updateGuards.filter((g) => g.name !== "None").length
|
|
107
|
+
? { updateGuards: sliceCls.updateGuards.map((g) => g.name) }
|
|
108
|
+
: {}),
|
|
109
|
+
...(sliceCls.removeGuards !== sliceCls.cruGuards && sliceCls.removeGuards.filter((g) => g.name !== "None").length
|
|
110
|
+
? { removeGuards: sliceCls.removeGuards.map((g) => g.name) }
|
|
111
|
+
: {}),
|
|
102
112
|
endpoint,
|
|
103
113
|
};
|
|
104
114
|
}
|
package/signal/signalContext.ts
CHANGED
|
@@ -341,6 +341,11 @@ export class SignalContext<
|
|
|
341
341
|
getEnv() {
|
|
342
342
|
return this.#env;
|
|
343
343
|
}
|
|
344
|
+
getArg<T = unknown>(argName: string): T | undefined {
|
|
345
|
+
const index = this.endpointInfo.args.findIndex((arg) => arg.name === argName);
|
|
346
|
+
if (index === -1) return undefined;
|
|
347
|
+
return this.args[index] as T;
|
|
348
|
+
}
|
|
344
349
|
}
|
|
345
350
|
|
|
346
351
|
export class HttpExecutionContext<Appended = unknown> {
|
package/signal/slice.ts
CHANGED
|
@@ -35,6 +35,9 @@ export type SliceCls<
|
|
|
35
35
|
[SLICE_META]: SliceInfoObj;
|
|
36
36
|
getGuards: GuardCls[];
|
|
37
37
|
cruGuards: GuardCls[];
|
|
38
|
+
createGuards: GuardCls[];
|
|
39
|
+
updateGuards: GuardCls[];
|
|
40
|
+
removeGuards: GuardCls[];
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
interface RootSliceOption {
|
|
@@ -42,6 +45,9 @@ interface RootSliceOption {
|
|
|
42
45
|
root?: Cls<Guard> | Cls<Guard>[];
|
|
43
46
|
get?: Cls<Guard> | Cls<Guard>[];
|
|
44
47
|
cru?: Cls<Guard> | Cls<Guard>[];
|
|
48
|
+
create?: Cls<Guard> | Cls<Guard>[];
|
|
49
|
+
update?: Cls<Guard> | Cls<Guard>[];
|
|
50
|
+
remove?: Cls<Guard> | Cls<Guard>[];
|
|
45
51
|
};
|
|
46
52
|
prefix?: string;
|
|
47
53
|
}
|
|
@@ -123,21 +129,14 @@ export function slice<
|
|
|
123
129
|
srv.cnst.insight,
|
|
124
130
|
srv.db.filter,
|
|
125
131
|
);
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
: [option.guards.get]
|
|
135
|
-
: [];
|
|
136
|
-
const cruGuards = option.guards?.cru
|
|
137
|
-
? Array.isArray(option.guards.cru)
|
|
138
|
-
? option.guards.cru
|
|
139
|
-
: [option.guards.cru]
|
|
140
|
-
: [];
|
|
132
|
+
const toGuards = (guard?: Cls<Guard> | Cls<Guard>[]) => (guard ? (Array.isArray(guard) ? guard : [guard]) : []);
|
|
133
|
+
const rootGuards = toGuards(option.guards?.root);
|
|
134
|
+
const getGuards = toGuards(option.guards?.get);
|
|
135
|
+
const cruGuards = toGuards(option.guards?.cru);
|
|
136
|
+
|
|
137
|
+
const createGuards = option.guards?.create ? toGuards(option.guards.create) : cruGuards;
|
|
138
|
+
const updateGuards = option.guards?.update ? toGuards(option.guards.update) : cruGuards;
|
|
139
|
+
const removeGuards = option.guards?.remove ? toGuards(option.guards.remove) : cruGuards;
|
|
141
140
|
const srvKeys = [
|
|
142
141
|
...new Set([...Object.keys(srv.srvMap), ...libSlices.flatMap((libSlice) => Object.keys(libSlice.srv.srvMap))]),
|
|
143
142
|
];
|
|
@@ -148,6 +147,9 @@ export function slice<
|
|
|
148
147
|
static srv = srv;
|
|
149
148
|
static getGuards = getGuards;
|
|
150
149
|
static cruGuards = cruGuards;
|
|
150
|
+
static createGuards = createGuards;
|
|
151
|
+
static updateGuards = updateGuards;
|
|
152
|
+
static removeGuards = removeGuards;
|
|
151
153
|
static [SLICE_META] = Object.assign(
|
|
152
154
|
{
|
|
153
155
|
[""]: init({ guards: rootGuards })
|
package/signal/types.ts
CHANGED
|
@@ -129,6 +129,9 @@ export interface SerializedSignal {
|
|
|
129
129
|
filter?: SerializedFilter;
|
|
130
130
|
getGuards?: string[];
|
|
131
131
|
cruGuards?: string[];
|
|
132
|
+
createGuards?: string[];
|
|
133
|
+
updateGuards?: string[];
|
|
134
|
+
removeGuards?: string[];
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
export type SignalType = "restapi" | "websocket";
|
|
@@ -190,6 +190,8 @@ export interface Route {
|
|
|
190
190
|
path: string;
|
|
191
191
|
renderPage?: RouteRender;
|
|
192
192
|
renderLayout?: RouteRender;
|
|
193
|
+
/** Synthetic layout render from a `_overrides.tsx` at this node; wraps the subtree in a UI-override provider. */
|
|
194
|
+
renderOverrides?: RouteRender;
|
|
193
195
|
pageIncludesOwnLayout?: boolean;
|
|
194
196
|
isSpecialRoute?: boolean;
|
|
195
197
|
loader?: () => unknown;
|
|
@@ -48,6 +48,7 @@ export declare class SignalContext<Ctx extends HttpExecutionContext | WebSocketE
|
|
|
48
48
|
getWebSocketContext<Appended = unknown>(): WebSocketExecutionContext<Appended>;
|
|
49
49
|
getRoomId(key: string): string;
|
|
50
50
|
getEnv(): Env;
|
|
51
|
+
getArg<T = unknown>(argName: string): T | undefined;
|
|
51
52
|
}
|
|
52
53
|
export declare class HttpExecutionContext<Appended = unknown> {
|
|
53
54
|
#private;
|
package/types/signal/slice.d.ts
CHANGED
|
@@ -27,12 +27,18 @@ export type SliceCls<SrvModule extends ServiceModel = ServiceModel, SliceInfoObj
|
|
|
27
27
|
[SLICE_META]: SliceInfoObj;
|
|
28
28
|
getGuards: GuardCls[];
|
|
29
29
|
cruGuards: GuardCls[];
|
|
30
|
+
createGuards: GuardCls[];
|
|
31
|
+
updateGuards: GuardCls[];
|
|
32
|
+
removeGuards: GuardCls[];
|
|
30
33
|
};
|
|
31
34
|
interface RootSliceOption {
|
|
32
35
|
guards?: {
|
|
33
36
|
root?: Cls<Guard> | Cls<Guard>[];
|
|
34
37
|
get?: Cls<Guard> | Cls<Guard>[];
|
|
35
38
|
cru?: Cls<Guard> | Cls<Guard>[];
|
|
39
|
+
create?: Cls<Guard> | Cls<Guard>[];
|
|
40
|
+
update?: Cls<Guard> | Cls<Guard>[];
|
|
41
|
+
remove?: Cls<Guard> | Cls<Guard>[];
|
|
36
42
|
};
|
|
37
43
|
prefix?: string;
|
|
38
44
|
}
|
package/types/signal/types.d.ts
CHANGED
|
@@ -122,6 +122,9 @@ export interface SerializedSignal {
|
|
|
122
122
|
filter?: SerializedFilter;
|
|
123
123
|
getGuards?: string[];
|
|
124
124
|
cruGuards?: string[];
|
|
125
|
+
createGuards?: string[];
|
|
126
|
+
updateGuards?: string[];
|
|
127
|
+
removeGuards?: string[];
|
|
125
128
|
}
|
|
126
129
|
export type SignalType = "restapi" | "websocket";
|
|
127
130
|
export type WebsocketReqData = {
|
package/types/ui/Button.d.ts
CHANGED
|
@@ -8,4 +8,9 @@ export type ButtonProps<Result> = Omit<ButtonHTMLAttributes<HTMLButtonElement>,
|
|
|
8
8
|
/** Called after the button briefly enters success state. */
|
|
9
9
|
onSuccess?: (result: Result) => void;
|
|
10
10
|
};
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Async-aware button. Resolves to a route-scoped override when a `page/**\/_overrides.tsx`
|
|
13
|
+
* in the route's ancestry declares one, otherwise renders {@link DefaultButton}. The public
|
|
14
|
+
* generic signature is preserved, so `<Button<Todo> onSuccess={(r: Todo) => …} />` still infers.
|
|
15
|
+
*/
|
|
16
|
+
export declare const Button: <Result = unknown>(props: ButtonProps<Result>) => React.ReactElement<ButtonProps<Result>, string | React.JSXElementConstructor<any>>;
|
package/types/ui/DatePicker.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Dayjs } from "akanjs/base";
|
|
2
|
-
interface DatePickerProps {
|
|
2
|
+
export interface DatePickerProps {
|
|
3
3
|
value?: Dayjs | null;
|
|
4
4
|
onChange: (value: Dayjs | null) => void;
|
|
5
5
|
showTime?: boolean;
|
|
@@ -10,12 +10,7 @@ interface DatePickerProps {
|
|
|
10
10
|
placement?: "top" | "bottom" | "left" | "right";
|
|
11
11
|
defaultValue?: Dayjs;
|
|
12
12
|
}
|
|
13
|
-
export
|
|
14
|
-
({ value, onChange, showTime, format, timeIntervals, disabledDate, placement, className, defaultValue, }: DatePickerProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
-
RangePicker: ({ value, onChange, format, showTime, timeIntervals, disabledDate, className, }: RangePickerProps) => import("react/jsx-runtime").JSX.Element;
|
|
16
|
-
TimePicker: ({ disabled, className, value, format, onChange, timeIntervals, }: TimePickerProps) => import("react/jsx-runtime").JSX.Element;
|
|
17
|
-
};
|
|
18
|
-
interface RangePickerProps {
|
|
13
|
+
export interface RangePickerProps {
|
|
19
14
|
value: [Dayjs | null, Dayjs | null];
|
|
20
15
|
onChange: (value: [Dayjs | null, Dayjs | null]) => void;
|
|
21
16
|
format?: string;
|
|
@@ -24,7 +19,7 @@ interface RangePickerProps {
|
|
|
24
19
|
disabledDate?: (date: Dayjs) => boolean | null | undefined;
|
|
25
20
|
className?: string;
|
|
26
21
|
}
|
|
27
|
-
interface TimePickerProps {
|
|
22
|
+
export interface TimePickerProps {
|
|
28
23
|
value: Dayjs | null;
|
|
29
24
|
onChange: (value: Dayjs) => void;
|
|
30
25
|
format?: string;
|
|
@@ -33,4 +28,12 @@ interface TimePickerProps {
|
|
|
33
28
|
className?: string;
|
|
34
29
|
disabled?: boolean;
|
|
35
30
|
}
|
|
36
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Date picker. `DatePicker`, `DatePicker.RangePicker`, and `DatePicker.TimePicker` each resolve to a
|
|
33
|
+
* route-scoped override when a `page/**\/_overrides.tsx` in the route's ancestry declares one (slots
|
|
34
|
+
* `DatePicker`, `DatePickerRangePicker`, `DatePickerTimePicker`).
|
|
35
|
+
*/
|
|
36
|
+
export declare const DatePicker: import("react").ComponentType<DatePickerProps> & {
|
|
37
|
+
RangePicker: import("react").ComponentType<RangePickerProps>;
|
|
38
|
+
TimePicker: import("react").ComponentType<TimePickerProps>;
|
|
39
|
+
};
|
package/types/ui/Dropdown.d.ts
CHANGED
|
@@ -11,4 +11,9 @@ export interface DropdownProps {
|
|
|
11
11
|
/** Additional classes for the dropdown content panel. */
|
|
12
12
|
dropdownClassName?: string;
|
|
13
13
|
}
|
|
14
|
-
export declare const
|
|
14
|
+
export declare const DefaultDropdown: ({ value, content, className, buttonClassName, dropdownClassName }: DropdownProps) => import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
/**
|
|
16
|
+
* Dropdown. Resolves to a route-scoped override when a `page/**\/_overrides.tsx`
|
|
17
|
+
* in the route's ancestry declares one, otherwise renders {@link DefaultDropdown}.
|
|
18
|
+
*/
|
|
19
|
+
export declare const Dropdown: import("react").ComponentType<DropdownProps>;
|
package/types/ui/Empty.d.ts
CHANGED
|
@@ -9,4 +9,10 @@ export interface EmptyProps {
|
|
|
9
9
|
/** Minimum empty-state height in pixels. */
|
|
10
10
|
minHeight?: number;
|
|
11
11
|
}
|
|
12
|
-
export declare const
|
|
12
|
+
export declare const DefaultEmpty: ({ className, description, children, minHeight }: EmptyProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
/**
|
|
14
|
+
* Empty-state placeholder. Resolves to a route-scoped override when a
|
|
15
|
+
* `page/**\/_overrides.tsx` in the route's ancestry declares one, otherwise
|
|
16
|
+
* renders {@link DefaultEmpty}.
|
|
17
|
+
*/
|
|
18
|
+
export declare const Empty: import("react").ComponentType<EmptyProps>;
|
package/types/ui/Input.d.ts
CHANGED
|
@@ -24,14 +24,6 @@ export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "
|
|
|
24
24
|
/** Called when Escape is pressed after blurring the input. */
|
|
25
25
|
onPressEscape?: (e: KeyboardEvent<HTMLInputElement>) => void;
|
|
26
26
|
};
|
|
27
|
-
export declare const Input: {
|
|
28
|
-
({ className, nullable, inputRef, value, cacheKey, inputStyleType, icon, iconClassName, inputClassName, inputWrapperClassName, onPressEnter, onPressEscape, onChange, validate, ...rest }: InputProps): import("react/jsx-runtime").JSX.Element;
|
|
29
|
-
TextArea: ({ className, nullable, value, inputClassName, inputWrapperClassName, cacheKey, onPressEnter, onPressEscape, onChange, validate, ...rest }: TextAreaProps) => import("react/jsx-runtime").JSX.Element;
|
|
30
|
-
Password: ({ className, nullable, value, icon, iconClassName, inputClassName, inputWrapperClassName, cacheKey, onPressEnter, onPressEscape, onChange, validate, ...rest }: PasswordProps) => import("react/jsx-runtime").JSX.Element;
|
|
31
|
-
Email: ({ inputStyleType, className, nullable, value, cacheKey, onPressEnter, onPressEscape, onChange, validate, icon, iconClassName, inputClassName, inputWrapperClassName, ...rest }: EmailProps) => import("react/jsx-runtime").JSX.Element;
|
|
32
|
-
Number: ({ className, nullable, value, icon, cacheKey, iconClassName, inputClassName, inputWrapperClassName, numberFormat, onPressEnter, onPressEscape, validate, onChange, formatter, parser, ...rest }: NumberProps) => import("react/jsx-runtime").JSX.Element;
|
|
33
|
-
Checkbox: ({ checked, onChange, className, ...rest }: CheckboxProps) => import("react/jsx-runtime").JSX.Element;
|
|
34
|
-
};
|
|
35
27
|
export type TextAreaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "value" | "type" | "onChange" | "onPressEnter"> & {
|
|
36
28
|
inputRef?: RefObject<HTMLTextAreaElement | null>;
|
|
37
29
|
value: string;
|
|
@@ -93,3 +85,15 @@ export type CheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, "onChang
|
|
|
93
85
|
checked: boolean;
|
|
94
86
|
onChange: (checked: boolean, e: ChangeEvent<HTMLInputElement>) => void;
|
|
95
87
|
};
|
|
88
|
+
/**
|
|
89
|
+
* Text input plus its field variants. Each leaf resolves to a route-scoped override when a
|
|
90
|
+
* `page/**\/_overrides.tsx` in the route's ancestry declares one (slots `Input`, `InputTextArea`,
|
|
91
|
+
* `InputPassword`, `InputEmail`, `InputNumber`, `InputCheckbox`), otherwise renders the default.
|
|
92
|
+
*/
|
|
93
|
+
export declare const Input: React.ComponentType<InputProps> & {
|
|
94
|
+
TextArea: React.ComponentType<TextAreaProps>;
|
|
95
|
+
Password: React.ComponentType<PasswordProps>;
|
|
96
|
+
Email: React.ComponentType<EmailProps>;
|
|
97
|
+
Number: React.ComponentType<NumberProps>;
|
|
98
|
+
Checkbox: React.ComponentType<CheckboxProps>;
|
|
99
|
+
};
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading indicators. Each member is an independent override slot (`LoadingSpin`, `LoadingSkeleton`,
|
|
3
|
+
* `LoadingProgressBar`, `LoadingButton`, `LoadingInput`, `LoadingArea`), resolved from the closest
|
|
4
|
+
* `page/**\/_overrides.tsx` in the route's ancestry, otherwise the shipped default.
|
|
5
|
+
*/
|
|
1
6
|
export declare const Loading: {
|
|
2
|
-
Area:
|
|
3
|
-
Button: (
|
|
4
|
-
Input: (
|
|
5
|
-
ProgressBar: (
|
|
6
|
-
Skeleton: (
|
|
7
|
-
Spin: (
|
|
7
|
+
Area: import("react").ComponentType<Record<string, never>>;
|
|
8
|
+
Button: import("react").ComponentType<import("./Button.d.ts").LoadingProps>;
|
|
9
|
+
Input: import("react").ComponentType<import("./Input.d.ts").LoadingProps>;
|
|
10
|
+
ProgressBar: import("react").ComponentType<import("./ProgressBar.d.ts").ProgressBarProps>;
|
|
11
|
+
Skeleton: import("react").ComponentType<import("./Skeleton.d.ts").SkeletonProps>;
|
|
12
|
+
Spin: import("react").ComponentType<import("./Spin.d.ts").SpinProps>;
|
|
8
13
|
};
|
package/types/ui/Menu.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
interface MenuItem {
|
|
2
|
+
export interface MenuItem {
|
|
3
3
|
label: ReactNode;
|
|
4
4
|
key: string;
|
|
5
5
|
children?: MenuItem[];
|
|
6
6
|
icon?: ReactNode;
|
|
7
7
|
type?: string;
|
|
8
8
|
}
|
|
9
|
-
interface MenuProps {
|
|
9
|
+
export interface MenuProps {
|
|
10
10
|
className?: string;
|
|
11
11
|
ulClassName?: string;
|
|
12
12
|
liClassName?: string;
|
|
@@ -22,5 +22,10 @@ interface MenuProps {
|
|
|
22
22
|
onMouseOver?: () => void;
|
|
23
23
|
onMouseLeave?: () => void;
|
|
24
24
|
}
|
|
25
|
-
export declare const
|
|
26
|
-
|
|
25
|
+
export declare const DefaultMenu: ({ items, onClick, selectedKeys, labelClassName, defaultSelectedKeys, className, ulClassName, liClassName, style, mode, activeStyle, inlineCollapsed, onMouseOver, onMouseLeave, }: MenuProps) => import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
/**
|
|
27
|
+
* Navigation menu. Resolves to a route-scoped override when a
|
|
28
|
+
* `page/**\/_overrides.tsx` in the route's ancestry declares one, otherwise
|
|
29
|
+
* renders {@link DefaultMenu}.
|
|
30
|
+
*/
|
|
31
|
+
export declare const Menu: import("react").ComponentType<MenuProps>;
|