cabloy 5.1.167 → 5.1.169
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/.cabloy-version +1 -1
- package/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/repo-e2e/specs/cabloy-basic.spec.ts +75 -0
- package/vona/pnpm-lock.yaml +267 -53
- package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +2 -0
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +113 -84
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/component/routedDialog.ts +2 -1
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/component/routedDialog/controller.tsx +8 -2
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/service/appModal.ts +60 -8
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/types/appModal.ts +43 -8
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/.metadata/index.ts +6 -6
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +95 -10
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/index.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +35 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +24 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/.metadata/component/blockResourcePickerActions.ts +34 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/.metadata/index.ts +30 -8
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/.metadata/locales.ts +7 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/component/blockResourcePickerActions/controller.tsx +92 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/component/formFieldResourcePicker/controller.tsx +224 -11
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/config/locale/en-us.ts +8 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/config/locale/zh-cn.ts +7 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/index.ts +2 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/lib/resourcePicker.ts +29 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/lib/resourcePickerPageHost.ts +64 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/test/lib/resourcePicker.test.ts +32 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/test/lib/resourcePickerPageHost.test.ts +94 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +18 -12
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/.metadata/component/resourcePickerContext.ts +31 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/.metadata/index.ts +48 -5
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/.metadata/page/resourcePicker.ts +16 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/component/resourcePickerContext/controller.tsx +26 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/index.ts +2 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/resourcePicker.ts +43 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/resourcePicker/controller.tsx +85 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/routes.ts +9 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-bean/cli/controller/metadata/generateFileComponent.ts +40 -1
- package/zova/src/suite-vendor/a-zova/modules/a-bean/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routerViewBase.tsx +5 -0
- package/zova/src/suite-vendor/a-zova/modules/a-routerstack/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-routerstack/src/component/routerViewStack/controller.tsx +7 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +2 -0
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +13 -10
- package/zova/src/suite-vendor/a-zova/package.json +5 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// eslint-disable
|
|
2
2
|
/** controller: begin */
|
|
3
|
+
export * from '../component/blockResourcePickerActions/controller.jsx';
|
|
3
4
|
export * from '../component/formFieldResourcePicker/controller.jsx';
|
|
4
5
|
|
|
5
6
|
import 'zova';
|
|
@@ -9,6 +10,11 @@ declare module 'zova' {
|
|
|
9
10
|
}
|
|
10
11
|
declare module 'zova-module-basic-resource' {
|
|
11
12
|
|
|
13
|
+
export interface ControllerBlockResourcePickerActions {
|
|
14
|
+
/** @internal */
|
|
15
|
+
get scope(): ScopeModuleBasicResource;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
export interface ControllerFormFieldResourcePicker {
|
|
13
19
|
/** @internal */
|
|
14
20
|
get scope(): ScopeModuleBasicResource;
|
|
@@ -16,28 +22,35 @@ declare module 'zova-module-basic-resource' {
|
|
|
16
22
|
}
|
|
17
23
|
/** controller: end */
|
|
18
24
|
/** controller: begin */
|
|
19
|
-
import {
|
|
25
|
+
import type { ControllerBlockResourcePickerActions } from '../component/blockResourcePickerActions/controller.jsx';
|
|
26
|
+
import type { ControllerFormFieldResourcePicker } from '../component/formFieldResourcePicker/controller.jsx';
|
|
20
27
|
import 'zova';
|
|
21
28
|
declare module 'zova' {
|
|
22
29
|
export interface IBeanRecordLocal {
|
|
23
|
-
'basic-resource.controller.
|
|
30
|
+
'basic-resource.controller.blockResourcePickerActions': ControllerBlockResourcePickerActions;
|
|
31
|
+
'basic-resource.controller.formFieldResourcePicker': ControllerFormFieldResourcePicker;
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
/** controller: end */
|
|
27
35
|
|
|
28
36
|
/** components: begin */
|
|
37
|
+
export * from './component/blockResourcePickerActions.js';
|
|
38
|
+
import { ZBlockResourcePickerActions } from './component/blockResourcePickerActions.js';
|
|
29
39
|
export * from './component/formFieldResourcePicker.js';
|
|
30
40
|
import { ZFormFieldResourcePicker } from './component/formFieldResourcePicker.js';
|
|
31
41
|
export const components = {
|
|
32
|
-
'
|
|
42
|
+
'blockResourcePickerActions': ZBlockResourcePickerActions,
|
|
43
|
+
'formFieldResourcePicker': ZFormFieldResourcePicker,
|
|
33
44
|
};
|
|
34
45
|
import 'zova';
|
|
35
46
|
declare module 'zova' {
|
|
36
47
|
export interface IComponentRecord {
|
|
37
|
-
'basic-resource:
|
|
48
|
+
'basic-resource:blockResourcePickerActions': ControllerBlockResourcePickerActions;
|
|
49
|
+
'basic-resource:formFieldResourcePicker': ControllerFormFieldResourcePicker;
|
|
38
50
|
}
|
|
39
51
|
export interface IZovaComponentRecord {
|
|
40
|
-
'basic-resource:
|
|
52
|
+
'basic-resource:blockResourcePickerActions': typeof ZBlockResourcePickerActions;
|
|
53
|
+
'basic-resource:formFieldResourcePicker': typeof ZFormFieldResourcePicker;
|
|
41
54
|
}
|
|
42
55
|
}
|
|
43
56
|
/** components: end */
|
|
@@ -68,7 +81,7 @@ declare module 'zova-module-basic-resource' {
|
|
|
68
81
|
}
|
|
69
82
|
/** tableCell: end */
|
|
70
83
|
/** tableCell: begin */
|
|
71
|
-
import { TableCellResourcePicker } from '../bean/tableCell.resourcePicker.jsx';
|
|
84
|
+
import type { TableCellResourcePicker } from '../bean/tableCell.resourcePicker.jsx';
|
|
72
85
|
import 'zova';
|
|
73
86
|
declare module 'zova' {
|
|
74
87
|
export interface IBeanRecordGeneral {
|
|
@@ -76,8 +89,11 @@ declare module 'zova' {
|
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
/** tableCell: end */
|
|
92
|
+
/** locale: begin */
|
|
93
|
+
import { locales } from './locales.js';
|
|
94
|
+
/** locale: end */
|
|
79
95
|
/** scope: begin */
|
|
80
|
-
import { BeanScopeBase, type BeanScopeUtil } from 'zova';
|
|
96
|
+
import { BeanScopeBase, type BeanScopeUtil, TypeModuleLocales, TypeLocaleBase } from 'zova';
|
|
81
97
|
import { Scope } from 'zova-module-a-bean';
|
|
82
98
|
|
|
83
99
|
@Scope()
|
|
@@ -85,6 +101,7 @@ export class ScopeModuleBasicResource extends BeanScopeBase {}
|
|
|
85
101
|
|
|
86
102
|
export interface ScopeModuleBasicResource {
|
|
87
103
|
util: BeanScopeUtil;
|
|
104
|
+
locale: TypeModuleLocales<(typeof locales)[TypeLocaleBase]>;
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
import 'zova';
|
|
@@ -95,9 +112,14 @@ declare module 'zova' {
|
|
|
95
112
|
|
|
96
113
|
|
|
97
114
|
|
|
98
|
-
|
|
115
|
+
export interface IBeanScopeLocale {
|
|
116
|
+
'basic-resource': (typeof locales)[TypeLocaleBase];
|
|
117
|
+
}
|
|
99
118
|
|
|
100
119
|
|
|
101
120
|
}
|
|
102
121
|
|
|
122
|
+
export function locale<K extends keyof (typeof locales)[TypeLocaleBase]>(key: K): `basic-resource::${K}` {
|
|
123
|
+
return `basic-resource::${key}`;
|
|
124
|
+
}
|
|
103
125
|
/** scope: end */
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { IComponentOptions } from 'zova';
|
|
2
|
+
import type { IJsxRenderContextPage, IResourceBlockOptionsBase } from 'zova-module-a-openapi';
|
|
3
|
+
import type { IResourcePickerPageContext } from 'zova-module-rest-resource';
|
|
4
|
+
|
|
5
|
+
import { BeanControllerBase, Use } from 'zova';
|
|
6
|
+
import { Controller } from 'zova-module-a-bean';
|
|
7
|
+
import { resolvePickerSelectionMax } from 'zova-module-basic-page';
|
|
8
|
+
import { resourcePickerPageContextKey } from 'zova-module-rest-resource';
|
|
9
|
+
|
|
10
|
+
declare module 'zova-module-a-openapi' {
|
|
11
|
+
export interface IResourceBlockRecord {
|
|
12
|
+
'basic-resource:blockResourcePickerActions'?: ControllerBlockResourcePickerActionsProps;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ControllerBlockResourcePickerActionsProps extends IResourceBlockOptionsBase {}
|
|
17
|
+
|
|
18
|
+
@Controller()
|
|
19
|
+
export class ControllerBlockResourcePickerActions extends BeanControllerBase {
|
|
20
|
+
static $propsDefault = {
|
|
21
|
+
class: 'flex items-center justify-between gap-3',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static $componentOptions: IComponentOptions = { inheritAttrs: false, deepExtendDefault: true };
|
|
25
|
+
|
|
26
|
+
@Use({ injectionScope: 'host' })
|
|
27
|
+
$$renderContext: IJsxRenderContextPage;
|
|
28
|
+
|
|
29
|
+
@Use({ name: resourcePickerPageContextKey, injectionScope: 'host' })
|
|
30
|
+
$$pickerContext: IResourcePickerPageContext;
|
|
31
|
+
|
|
32
|
+
protected async __init__() {}
|
|
33
|
+
|
|
34
|
+
get pickerOptions() {
|
|
35
|
+
return this.$$pickerContext.options;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get selectionMax() {
|
|
39
|
+
return resolvePickerSelectionMax(
|
|
40
|
+
this.pickerOptions.selectionMode,
|
|
41
|
+
this.pickerOptions.selectionMax,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get selection() {
|
|
46
|
+
return this.$$renderContext.$$page.selection.ids;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get canConfirm() {
|
|
50
|
+
const count = this.selection.length;
|
|
51
|
+
return (
|
|
52
|
+
count > 0 &&
|
|
53
|
+
count <= this.selectionMax &&
|
|
54
|
+
(this.pickerOptions.selectionMode !== 'single' || count === 1)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public confirm() {
|
|
59
|
+
if (!this.canConfirm) return;
|
|
60
|
+
this.$$pickerContext.dialog.resolve(this.$$renderContext.$$page.selection);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public cancel() {
|
|
64
|
+
this.$$pickerContext.dialog.cancel();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected render() {
|
|
68
|
+
const count = this.selection.length;
|
|
69
|
+
return (
|
|
70
|
+
<div class="flex items-center justify-between gap-3">
|
|
71
|
+
<span class="text-sm text-base-content/70" role="status" aria-live="polite">
|
|
72
|
+
{this.pickerOptions.selectionMode === 'multiple'
|
|
73
|
+
? this.scope.locale.SelectedItemsWithMax(count, this.selectionMax)
|
|
74
|
+
: this.scope.locale.SelectedItems(count)}
|
|
75
|
+
</span>
|
|
76
|
+
<div class="join">
|
|
77
|
+
<button type="button" class="btn join-item" onClick={() => this.cancel()}>
|
|
78
|
+
{this.scope.locale.Cancel()}
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
class="btn btn-primary join-item"
|
|
83
|
+
disabled={!this.canConfirm}
|
|
84
|
+
onClick={() => this.confirm()}
|
|
85
|
+
>
|
|
86
|
+
{this.scope.locale.Select()}
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -1,15 +1,36 @@
|
|
|
1
|
+
import type { TableIdentity } from 'table-identity';
|
|
1
2
|
import type { IComponentOptions } from 'zova';
|
|
2
3
|
import type { IFormFieldComponentOptions, IJsxRenderContextFormField } from 'zova-module-a-form';
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
IResourceFormFieldOptionsBase,
|
|
6
|
+
IResourceTableSelectionPayload,
|
|
7
|
+
ITableQuery,
|
|
8
|
+
} from 'zova-module-a-openapi';
|
|
9
|
+
import type {
|
|
10
|
+
IResourcePickerPageOptions,
|
|
11
|
+
IResourcePickerPageSession,
|
|
12
|
+
} from 'zova-module-rest-resource';
|
|
4
13
|
|
|
5
14
|
import { classes } from 'typestyle';
|
|
6
15
|
import { BeanControllerBase, Use } from 'zova';
|
|
7
16
|
import { Controller } from 'zova-module-a-bean';
|
|
8
17
|
import { ZFormField, ZFormFieldPreset } from 'zova-module-a-form';
|
|
9
18
|
import { $QueryEnsureLoaded } from 'zova-module-a-model';
|
|
19
|
+
import { resolvePickerSelectionMax } from 'zova-module-basic-page';
|
|
10
20
|
import { ZSelect, ZSelectProps } from 'zova-module-basic-select';
|
|
21
|
+
import { resourcePickerPageHostKey } from 'zova-module-rest-resource';
|
|
11
22
|
import { ModelResource } from 'zova-module-rest-resource';
|
|
12
23
|
|
|
24
|
+
import type { TypeResourcePickerSelectionMode } from '../../lib/resourcePicker.js';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
normalizeResourcePickerIds,
|
|
28
|
+
resolveResourcePickerValue,
|
|
29
|
+
} from '../../lib/resourcePicker.js';
|
|
30
|
+
import { createResourcePickerPageHost } from '../../lib/resourcePickerPageHost.js';
|
|
31
|
+
|
|
32
|
+
export type TypeResourcePickerMode = 'select' | 'routedDialog';
|
|
33
|
+
|
|
13
34
|
declare module 'zova-module-a-openapi' {
|
|
14
35
|
export interface IResourceFormFieldRecord {
|
|
15
36
|
'basic-resource:formFieldResourcePicker'?: IResourceFormFieldResourcePickerOptions;
|
|
@@ -22,6 +43,9 @@ export interface IResourceFormFieldResourcePickerOptions extends IResourceFormFi
|
|
|
22
43
|
query?: ITableQuery;
|
|
23
44
|
relationName?: string;
|
|
24
45
|
selectOptions?: ZSelectProps;
|
|
46
|
+
pickerMode?: TypeResourcePickerMode;
|
|
47
|
+
selectionMode?: TypeResourcePickerSelectionMode;
|
|
48
|
+
selectionMax?: number;
|
|
25
49
|
}
|
|
26
50
|
|
|
27
51
|
export interface ControllerFormFieldResourcePickerProps extends IFormFieldComponentOptions {
|
|
@@ -37,21 +61,21 @@ export class ControllerFormFieldResourcePicker extends BeanControllerBase {
|
|
|
37
61
|
static $componentOptions: IComponentOptions = { inheritAttrs: false, deepExtendDefault: true };
|
|
38
62
|
|
|
39
63
|
$$modelResource: ModelResource;
|
|
64
|
+
private _openingDialog = false;
|
|
65
|
+
private _pendingLabelIds = new Set<string>();
|
|
66
|
+
private _labelHydrationAttemptedIds = new Set<string>();
|
|
40
67
|
|
|
41
68
|
@Use({ injectionScope: 'host' })
|
|
42
69
|
$$renderContext: IJsxRenderContextFormField;
|
|
43
70
|
|
|
44
71
|
protected async __init__() {
|
|
45
72
|
const { ctx } = this.$$renderContext;
|
|
46
|
-
|
|
47
|
-
if (this.$props.readonly) return;
|
|
48
|
-
// modelResource
|
|
73
|
+
if (this.$props.readonly || this.pickerMode === 'routedDialog') return;
|
|
49
74
|
this.$$modelResource = await ctx.bean._getBeanSelector(
|
|
50
75
|
'rest-resource.model.resource',
|
|
51
76
|
true,
|
|
52
77
|
this.resource,
|
|
53
78
|
);
|
|
54
|
-
// load data
|
|
55
79
|
await $QueryEnsureLoaded(() => this.queryData);
|
|
56
80
|
}
|
|
57
81
|
|
|
@@ -65,6 +89,14 @@ export class ControllerFormFieldResourcePicker extends BeanControllerBase {
|
|
|
65
89
|
return this.$props.options;
|
|
66
90
|
}
|
|
67
91
|
|
|
92
|
+
get pickerMode(): TypeResourcePickerMode {
|
|
93
|
+
return this.resourcePickerOptions?.pickerMode ?? 'select';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get selectionMode(): TypeResourcePickerSelectionMode {
|
|
97
|
+
return this.resourcePickerOptions?.selectionMode ?? 'single';
|
|
98
|
+
}
|
|
99
|
+
|
|
68
100
|
get queryData() {
|
|
69
101
|
return this.$$modelResource.selectGeneral(
|
|
70
102
|
this.resourcePickerOptions?.actionPath,
|
|
@@ -86,6 +118,11 @@ export class ControllerFormFieldResourcePicker extends BeanControllerBase {
|
|
|
86
118
|
></ZFormFieldPreset>
|
|
87
119
|
);
|
|
88
120
|
}
|
|
121
|
+
if (this.pickerMode === 'routedDialog') return this._renderRoutedDialogField();
|
|
122
|
+
return this._renderSelectField();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private _renderSelectField() {
|
|
89
126
|
return (
|
|
90
127
|
<ZFormField
|
|
91
128
|
{...this.$props}
|
|
@@ -131,14 +168,190 @@ export class ControllerFormFieldResourcePicker extends BeanControllerBase {
|
|
|
131
168
|
);
|
|
132
169
|
}
|
|
133
170
|
|
|
171
|
+
private _renderRoutedDialogField() {
|
|
172
|
+
return (
|
|
173
|
+
<ZFormField
|
|
174
|
+
{...this.$props}
|
|
175
|
+
slotDefault={({ propsBucket, props }, $$formField) => {
|
|
176
|
+
const className = !propsBucket.needHandleBorder
|
|
177
|
+
? classes(
|
|
178
|
+
props.class,
|
|
179
|
+
'grow w-full h-full min-h-0 border-0 rounded-none bg-transparent px-0 py-0 text-left outline-none',
|
|
180
|
+
)
|
|
181
|
+
: classes(props.class, 'input', !$$formField.field.state.meta.isValid && 'input-error');
|
|
182
|
+
return (
|
|
183
|
+
<button
|
|
184
|
+
{...props}
|
|
185
|
+
type="button"
|
|
186
|
+
class={className}
|
|
187
|
+
onBlur={() => $$formField.handleBlur()}
|
|
188
|
+
onClick={() => {
|
|
189
|
+
void this._openPicker(propsBucket.value, value => {
|
|
190
|
+
$$formField.setValue(value, propsBucket.disableNotifyChanged);
|
|
191
|
+
}).catch(error => {
|
|
192
|
+
this.$errorHandler(error, 'ControllerFormFieldResourcePicker.openPicker');
|
|
193
|
+
});
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
{this._formatPickerValue(propsBucket.value) || this.scope.locale.PleaseSelect()}
|
|
197
|
+
</button>
|
|
198
|
+
);
|
|
199
|
+
}}
|
|
200
|
+
></ZFormField>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private async _openPicker(value: unknown, setValue: (value: unknown) => void) {
|
|
205
|
+
if (this._openingDialog) return;
|
|
206
|
+
this._openingDialog = true;
|
|
207
|
+
try {
|
|
208
|
+
const selectedIds = normalizeResourcePickerIds(value);
|
|
209
|
+
const options: IResourcePickerPageOptions = {
|
|
210
|
+
resource: this.resource,
|
|
211
|
+
actionPath: this.resourcePickerOptions?.actionPath,
|
|
212
|
+
query: this.resourcePickerOptions?.query,
|
|
213
|
+
selectionMode: this.selectionMode,
|
|
214
|
+
selectionMax: this.resourcePickerOptions?.selectionMax,
|
|
215
|
+
selectedIds,
|
|
216
|
+
};
|
|
217
|
+
const session: IResourcePickerPageSession = {
|
|
218
|
+
selectedIds: [...selectedIds],
|
|
219
|
+
selectedRows: [],
|
|
220
|
+
};
|
|
221
|
+
const handle = this.$appModal.routedDialog<
|
|
222
|
+
IResourceTableSelectionPayload,
|
|
223
|
+
IResourcePickerPageOptions,
|
|
224
|
+
IResourcePickerPageSession
|
|
225
|
+
>({
|
|
226
|
+
route: {
|
|
227
|
+
name: 'rest-resource:resourcePicker',
|
|
228
|
+
params: { resource: this.resource },
|
|
229
|
+
},
|
|
230
|
+
props: options,
|
|
231
|
+
session,
|
|
232
|
+
createPageHostProviders: dialog => ({
|
|
233
|
+
[resourcePickerPageHostKey]: createResourcePickerPageHost(dialog),
|
|
234
|
+
}),
|
|
235
|
+
});
|
|
236
|
+
const result = await handle.result;
|
|
237
|
+
if (!result) return;
|
|
238
|
+
const nextValue = resolveResourcePickerValue(
|
|
239
|
+
result.ids,
|
|
240
|
+
this.selectionMode,
|
|
241
|
+
resolvePickerSelectionMax(this.selectionMode, this.resourcePickerOptions?.selectionMax),
|
|
242
|
+
);
|
|
243
|
+
this._syncRelationField(nextValue, result.rows);
|
|
244
|
+
setValue(nextValue);
|
|
245
|
+
} finally {
|
|
246
|
+
this._openingDialog = false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private _formatPickerValue(value: unknown) {
|
|
251
|
+
const selectedIds = normalizeResourcePickerIds(value);
|
|
252
|
+
if (selectedIds.length === 0) return '';
|
|
253
|
+
const relationItems = this._getRelationItems();
|
|
254
|
+
const relationMap = new Map(
|
|
255
|
+
relationItems.map(item => [String(item.id as TableIdentity), item] as const),
|
|
256
|
+
);
|
|
257
|
+
const itemTitle = this._itemTitle;
|
|
258
|
+
const missingIds = selectedIds.filter(id => {
|
|
259
|
+
const item = relationMap.get(String(id));
|
|
260
|
+
return (
|
|
261
|
+
(item?.[itemTitle] === undefined || item[itemTitle] === null) &&
|
|
262
|
+
!this._labelHydrationAttemptedIds.has(String(id))
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
if (missingIds.length > 0) {
|
|
266
|
+
void this._hydrateMissingLabels(missingIds);
|
|
267
|
+
}
|
|
268
|
+
return selectedIds
|
|
269
|
+
.map(id => {
|
|
270
|
+
const title = relationMap.get(String(id))?.[itemTitle];
|
|
271
|
+
return title === undefined || title === null ? String(id) : String(title);
|
|
272
|
+
})
|
|
273
|
+
.join(', ');
|
|
274
|
+
}
|
|
275
|
+
|
|
134
276
|
private _getValueByRelation() {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
277
|
+
const item = this._getRelationItems()[0];
|
|
278
|
+
return item?.[this._itemTitle];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private get _itemTitle() {
|
|
282
|
+
return String(this.resourcePickerOptions?.selectOptions?.itemTitle ?? 'name');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private get _relationName() {
|
|
286
|
+
const relationName = this.resourcePickerOptions?.relationName;
|
|
287
|
+
if (relationName) return relationName;
|
|
288
|
+
const fieldName = this.$props.name;
|
|
289
|
+
const index = fieldName?.lastIndexOf('Id') ?? -1;
|
|
290
|
+
return index > 0 ? fieldName!.substring(0, index) : undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private _getRelationItems() {
|
|
294
|
+
const relationName = this._relationName;
|
|
295
|
+
if (!relationName) return [] as Record<string, unknown>[];
|
|
296
|
+
const value = this.$$renderContext.$$form.getFieldValue(relationName as never);
|
|
297
|
+
const items = Array.isArray(value) ? value : value ? [value] : [];
|
|
298
|
+
return items.filter(item => !!item && typeof item === 'object') as Record<string, unknown>[];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private _syncRelationField(value: unknown, rows: readonly Record<string, unknown>[]) {
|
|
302
|
+
const relationName = this._relationName;
|
|
303
|
+
if (!relationName) return;
|
|
304
|
+
const selectedIds = normalizeResourcePickerIds(value);
|
|
305
|
+
const itemTitle = this._itemTitle;
|
|
306
|
+
const rowMap = new Map(
|
|
307
|
+
this._getRelationItems().map(item => [String(item.id as TableIdentity), item] as const),
|
|
308
|
+
);
|
|
309
|
+
for (const row of rows) {
|
|
310
|
+
const id = row.id as TableIdentity;
|
|
311
|
+
if (id === undefined || id === null || id === '') continue;
|
|
312
|
+
const title = row[itemTitle];
|
|
313
|
+
if (title === undefined || title === null) continue;
|
|
314
|
+
rowMap.set(String(id), { id, [itemTitle]: title });
|
|
315
|
+
}
|
|
316
|
+
const relationItems = selectedIds.map(id => rowMap.get(String(id)) ?? { id });
|
|
317
|
+
const relationValue = this.selectionMode === 'multiple' ? relationItems : relationItems[0];
|
|
318
|
+
this.$$renderContext.$$form.setFieldValue(relationName as never, relationValue, true);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private async _hydrateMissingLabels(ids: readonly TableIdentity[]) {
|
|
322
|
+
const relationName = this._relationName;
|
|
323
|
+
if (!relationName) return;
|
|
324
|
+
const idsToLoad = ids.filter(id => {
|
|
325
|
+
const key = String(id);
|
|
326
|
+
if (this._pendingLabelIds.has(key) || this._labelHydrationAttemptedIds.has(key)) return false;
|
|
327
|
+
this._pendingLabelIds.add(key);
|
|
328
|
+
this._labelHydrationAttemptedIds.add(key);
|
|
329
|
+
return true;
|
|
330
|
+
});
|
|
331
|
+
if (idsToLoad.length === 0) return;
|
|
332
|
+
try {
|
|
333
|
+
const model = (await this.bean._getBeanSelector(
|
|
334
|
+
'rest-resource.model.resource',
|
|
335
|
+
true,
|
|
336
|
+
this.resource,
|
|
337
|
+
)) as ModelResource;
|
|
338
|
+
const rows = await Promise.all(
|
|
339
|
+
idsToLoad.map(async id => {
|
|
340
|
+
const result = await model.view(id).refetch();
|
|
341
|
+
return result.data;
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
const currentIds = normalizeResourcePickerIds(
|
|
345
|
+
this.$$renderContext.$$form.getFieldValue(this.$props.name as never),
|
|
346
|
+
);
|
|
347
|
+
this._syncRelationField(currentIds, rows.filter(Boolean) as Record<string, unknown>[]);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
this.$errorHandler(error, 'ControllerFormFieldResourcePicker.hydrateMissingLabels');
|
|
350
|
+
} finally {
|
|
351
|
+
for (const id of idsToLoad) {
|
|
352
|
+
this._pendingLabelIds.delete(String(id));
|
|
353
|
+
}
|
|
139
354
|
}
|
|
140
|
-
const obj = $$form.getFieldValue(relationName);
|
|
141
|
-
return obj?.[String(this.$props.options.selectOptions!.itemTitle)];
|
|
142
355
|
}
|
|
143
356
|
|
|
144
357
|
private _ensureEmptyItemFallback(
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TableIdentity } from 'table-identity';
|
|
2
|
+
|
|
3
|
+
import { isNil } from '@cabloy/utils';
|
|
4
|
+
|
|
5
|
+
export type TypeResourcePickerSelectionMode = 'single' | 'multiple';
|
|
6
|
+
|
|
7
|
+
export function normalizeResourcePickerIds(value: unknown): TableIdentity[] {
|
|
8
|
+
const values = Array.isArray(value) ? value : isNil(value) || value === '' ? [] : [value];
|
|
9
|
+
const ids: TableIdentity[] = [];
|
|
10
|
+
const keys = new Set<string>();
|
|
11
|
+
for (const value of values) {
|
|
12
|
+
if (isNil(value) || value === '') continue;
|
|
13
|
+
const id = value as TableIdentity;
|
|
14
|
+
const key = String(id);
|
|
15
|
+
if (keys.has(key)) continue;
|
|
16
|
+
keys.add(key);
|
|
17
|
+
ids.push(id);
|
|
18
|
+
}
|
|
19
|
+
return ids;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveResourcePickerValue(
|
|
23
|
+
value: unknown,
|
|
24
|
+
selectionMode: TypeResourcePickerSelectionMode,
|
|
25
|
+
selectionMax: number,
|
|
26
|
+
): TableIdentity | TableIdentity[] | undefined {
|
|
27
|
+
const selectedIds = normalizeResourcePickerIds(value).slice(0, selectionMax);
|
|
28
|
+
return selectionMode === 'single' ? selectedIds[0] : selectedIds;
|
|
29
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IResourceBlockOptionsBase,
|
|
3
|
+
IResourceRenderBlockOptionsBlock,
|
|
4
|
+
IResourceTableSelectionPayload,
|
|
5
|
+
} from 'zova-module-a-openapi';
|
|
6
|
+
import type { IRoutedDialogContext } from 'zova-module-basic-app';
|
|
7
|
+
import type {
|
|
8
|
+
IResourcePickerPageContext,
|
|
9
|
+
IResourcePickerPageHost,
|
|
10
|
+
} from 'zova-module-rest-resource';
|
|
11
|
+
|
|
12
|
+
import { deepExtend } from 'zova';
|
|
13
|
+
|
|
14
|
+
export function createResourcePickerPageHost(
|
|
15
|
+
dialog: IRoutedDialogContext<
|
|
16
|
+
IResourceTableSelectionPayload,
|
|
17
|
+
IResourcePickerPageContext['options'],
|
|
18
|
+
IResourcePickerPageContext['session']
|
|
19
|
+
>,
|
|
20
|
+
): IResourcePickerPageHost {
|
|
21
|
+
const options = dialog.props;
|
|
22
|
+
const session = dialog.session;
|
|
23
|
+
if (!options || !session) {
|
|
24
|
+
throw new Error('resource picker requires dialog options and session');
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
options,
|
|
28
|
+
session,
|
|
29
|
+
resolve: selection => dialog.resolve(selection),
|
|
30
|
+
cancel: () => dialog.cancel(),
|
|
31
|
+
prepareBlocks: (blocks, pickerContext) => {
|
|
32
|
+
if (!blocks) return;
|
|
33
|
+
return blocks.map(block => {
|
|
34
|
+
if (block.render !== 'basic-page:blockPage') return block;
|
|
35
|
+
return {
|
|
36
|
+
...block,
|
|
37
|
+
options: deepExtend({}, block.options, {
|
|
38
|
+
actionPath: pickerContext.options.actionPath,
|
|
39
|
+
queryFixed: pickerContext.options.query,
|
|
40
|
+
selectionPolicy: 'always',
|
|
41
|
+
selectionMode: pickerContext.options.selectionMode,
|
|
42
|
+
selectionMax: pickerContext.options.selectionMax,
|
|
43
|
+
selectedIds: pickerContext.session.selectedIds,
|
|
44
|
+
selectedRows: pickerContext.session.selectedRows,
|
|
45
|
+
onSelectionChange: (selection: IResourceTableSelectionPayload) => {
|
|
46
|
+
pickerContext.session.selectedIds = [...selection.ids];
|
|
47
|
+
pickerContext.session.selectedRows = selection.rows.map(row => ({ ...row }));
|
|
48
|
+
},
|
|
49
|
+
blocks: [
|
|
50
|
+
...((
|
|
51
|
+
block.options as IResourceBlockOptionsBase & {
|
|
52
|
+
blocks?: IResourceRenderBlockOptionsBlock[];
|
|
53
|
+
}
|
|
54
|
+
).blocks ?? []),
|
|
55
|
+
{
|
|
56
|
+
render: 'basic-resource:blockResourcePickerActions',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
normalizeResourcePickerIds,
|
|
6
|
+
resolveResourcePickerValue,
|
|
7
|
+
} from '../../src/lib/resourcePicker.ts';
|
|
8
|
+
|
|
9
|
+
test('resource picker normalization preserves identity types and deduplicates equivalent identities', () => {
|
|
10
|
+
assert.deepEqual(normalizeResourcePickerIds(undefined), []);
|
|
11
|
+
assert.deepEqual(normalizeResourcePickerIds(''), []);
|
|
12
|
+
assert.deepEqual(normalizeResourcePickerIds(7), [7]);
|
|
13
|
+
assert.deepEqual(normalizeResourcePickerIds([7, '7', 8, undefined, '']), [7, 8]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('resource picker result resolves a scalar identity for single mode', () => {
|
|
17
|
+
assert.equal(resolveResourcePickerValue([7, 8], 'single', 1), 7);
|
|
18
|
+
assert.equal(resolveResourcePickerValue([], 'single', 1), undefined);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('resource picker result enforces the configured multiple selection maximum', () => {
|
|
22
|
+
assert.deepEqual(resolveResourcePickerValue([1, 2, 3], 'multiple', 2), [1, 2]);
|
|
23
|
+
assert.deepEqual(resolveResourcePickerValue([1, 2], 'multiple', 0), []);
|
|
24
|
+
assert.deepEqual(
|
|
25
|
+
resolveResourcePickerValue(
|
|
26
|
+
Array.from({ length: 101 }, (_value, index) => index + 1),
|
|
27
|
+
'multiple',
|
|
28
|
+
100,
|
|
29
|
+
),
|
|
30
|
+
Array.from({ length: 100 }, (_value, index) => index + 1),
|
|
31
|
+
);
|
|
32
|
+
});
|