@rchemist/listgrid 0.2.11 → 0.2.13
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/dist/listgrid/components/fields/CustomOptionField.d.ts +8 -0
- package/dist/listgrid/components/fields/CustomOptionField.js +19 -5
- package/dist/listgrid/components/fields/ImageField.js +4 -3
- package/dist/listgrid/components/fields/MultipleAssetField.js +2 -1
- package/dist/listgrid/components/fields/PhoneNumberField.d.ts +8 -0
- package/dist/listgrid/components/fields/PhoneNumberField.js +20 -3
- package/dist/listgrid/components/fields/view/MultipleAssetUpload.js +1 -4
- package/dist/listgrid/components/fields/view/PhoneNumberFieldView.d.ts +3 -1
- package/dist/listgrid/components/fields/view/PhoneNumberFieldView.js +5 -6
- package/dist/listgrid/components/fields/view/PhoneNumberListView.d.ts +3 -1
- package/dist/listgrid/components/fields/view/PhoneNumberListView.js +4 -6
- package/dist/listgrid/components/fields/view/SmsModal.js +3 -2
- package/dist/listgrid/components/form/ui/ViewEntityFormButtons.js +16 -4
- package/dist/listgrid/components/list/ViewListGrid.js +9 -7
- package/dist/listgrid/components/revision/RevisionField.d.ts +4 -0
- package/dist/listgrid/components/revision/RevisionField.js +12 -4
- package/dist/listgrid/config/EntityForm.js +12 -11
- package/dist/listgrid/config/EntityFormButton.d.ts +8 -1
- package/dist/listgrid/config/RuntimeConfig.d.ts +58 -1
- package/dist/listgrid/config/RuntimeConfig.js +42 -2
- package/dist/listgrid/extensions/FieldExtensions.d.ts +25 -0
- package/dist/listgrid/extensions/FieldExtensions.js +26 -6
- package/dist/listgrid/index.d.ts +4 -3
- package/dist/listgrid/index.js +6 -4
- package/dist/listgrid/transfer/Provider/ExcelProvider.js +2 -1
- package/dist/listgrid/transfer/Type.js +3 -2
- package/dist/listgrid/utils/i18n.js +1 -1
- package/package.json +1 -1
|
@@ -9,7 +9,15 @@ interface CustomOptionFieldProps extends OptionalFieldProps {
|
|
|
9
9
|
export declare class CustomOptionField extends OptionalField<CustomOptionField> {
|
|
10
10
|
alias: string;
|
|
11
11
|
multiple?: boolean | undefined;
|
|
12
|
+
private fetchUrlOverride?;
|
|
13
|
+
private bulkFetchUrlOverride?;
|
|
12
14
|
constructor(name: string, order: number, alias: string, multiple?: boolean);
|
|
15
|
+
/** Override the single-alias fetch URL for this field instance. */
|
|
16
|
+
withFetchUrl(url: string): this;
|
|
17
|
+
/** Override the bulk-alias fetch URL for this field instance. */
|
|
18
|
+
withBulkFetchUrl(url: string): this;
|
|
19
|
+
getFetchUrl(): string;
|
|
20
|
+
getBulkFetchUrl(): string;
|
|
13
21
|
/**
|
|
14
22
|
* CustomOptionField 핵심 렌더링 로직
|
|
15
23
|
*/
|
|
@@ -8,18 +8,32 @@ import { getExternalApiDataWithError, isEmpty, isEquals } from '../../misc';
|
|
|
8
8
|
import { CheckBox } from '../../ui';
|
|
9
9
|
import { isTrue } from '../../utils/BooleanUtil';
|
|
10
10
|
import { MultiSelectBox } from '../../ui';
|
|
11
|
-
|
|
12
|
-
const customOptionBulkFetchUrl = '/option/by-aliases';
|
|
11
|
+
import { getEndpoint } from '../../config/RuntimeConfig';
|
|
13
12
|
// alias별 options 캐시 (동일 페이지 내에서 공유)
|
|
14
13
|
const customOptionCache = new Map();
|
|
15
14
|
export class CustomOptionField extends OptionalField {
|
|
16
15
|
constructor(name, order, alias, multiple) {
|
|
17
16
|
super(name, order, 'custom');
|
|
18
17
|
this.alias = alias;
|
|
19
|
-
// this.tooltip = <div>이 선택 옵션은 <a href='/academic/system/option'>시스템 옵션</a> 에서 시스템 ID <span style={{ fontWeight: `bold`, marginRight: `4px` }}>{alias}</span> 로 등록된 옵션값 입니다.</div>;
|
|
20
18
|
this.multiple = multiple ?? false;
|
|
21
19
|
this.layout = 'half';
|
|
22
20
|
}
|
|
21
|
+
/** Override the single-alias fetch URL for this field instance. */
|
|
22
|
+
withFetchUrl(url) {
|
|
23
|
+
this.fetchUrlOverride = url;
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
/** Override the bulk-alias fetch URL for this field instance. */
|
|
27
|
+
withBulkFetchUrl(url) {
|
|
28
|
+
this.bulkFetchUrlOverride = url;
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
getFetchUrl() {
|
|
32
|
+
return this.fetchUrlOverride ?? getEndpoint('customOptionByAlias');
|
|
33
|
+
}
|
|
34
|
+
getBulkFetchUrl() {
|
|
35
|
+
return this.bulkFetchUrlOverride ?? getEndpoint('customOptionByAliases');
|
|
36
|
+
}
|
|
23
37
|
/**
|
|
24
38
|
* CustomOptionField 핵심 렌더링 로직
|
|
25
39
|
*/
|
|
@@ -127,7 +141,7 @@ export async function getCustomOptionValues(alias) {
|
|
|
127
141
|
return customOptionCache.get(alias);
|
|
128
142
|
}
|
|
129
143
|
const response = await getExternalApiDataWithError({
|
|
130
|
-
url: `${
|
|
144
|
+
url: `${getEndpoint('customOptionByAlias')}/${alias}`,
|
|
131
145
|
method: 'GET',
|
|
132
146
|
});
|
|
133
147
|
// 데이터가 정상적으로 들어왔다면 옵션 데이터를 생성해 반환한다. 오류가 발생했다면(alias 가 없거나 하는 경우) 빈 배열을 반환한다.
|
|
@@ -153,7 +167,7 @@ export async function prefetchCustomOptions(aliases) {
|
|
|
153
167
|
const params = new URLSearchParams();
|
|
154
168
|
uncachedAliases.forEach((alias) => params.append('aliases', alias));
|
|
155
169
|
const response = await getExternalApiDataWithError({
|
|
156
|
-
url: `${
|
|
170
|
+
url: `${getEndpoint('customOptionByAliases')}?${params.toString()}`,
|
|
157
171
|
method: 'GET',
|
|
158
172
|
});
|
|
159
173
|
if (response.data && Array.isArray(response.data)) {
|
|
@@ -5,6 +5,7 @@ import { getInputRendererParameters } from '../helper/FieldRendererHelper';
|
|
|
5
5
|
import { isEmpty } from '../../utils';
|
|
6
6
|
import { getAccessableAssetUrl } from '../../misc';
|
|
7
7
|
import { TextInput } from '../../ui';
|
|
8
|
+
import { getEndpoint } from '../../config/RuntimeConfig';
|
|
8
9
|
export class ImageField extends ListableFormField {
|
|
9
10
|
constructor(name, order, config) {
|
|
10
11
|
super(name, order, 'file');
|
|
@@ -107,15 +108,15 @@ export class ImageField extends ListableFormField {
|
|
|
107
108
|
const imgUrl = getAccessableAssetUrl(file.existFiles[0].url);
|
|
108
109
|
return {
|
|
109
110
|
result: (_jsx("div", { className: "rcm-image-field-cell", children: _jsxs("div", { className: "rcm-image-field-hover-group", children: [_jsx("img", { className: "rcm-image-field-thumb", src: `${imgUrl}`, onError: (event) => {
|
|
110
|
-
event.currentTarget.src = '
|
|
111
|
+
event.currentTarget.src = getEndpoint('noImageFallback');
|
|
111
112
|
}, alt: "primary image" }), _jsx("div", { className: "rcm-image-field-preview-wrap", children: _jsx("img", { className: "rcm-image-field-preview", src: `${imgUrl}`, onError: (event) => {
|
|
112
|
-
event.currentTarget.src = '
|
|
113
|
+
event.currentTarget.src = getEndpoint('noImageFallback');
|
|
113
114
|
}, alt: "enlarged image" }) })] }) })),
|
|
114
115
|
};
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
return {
|
|
118
|
-
result: (_jsx("div", { className: "rcm-image-field-cell", children: _jsx("img", { className: "rcm-image-field-thumb rcm-image-field-thumb-placeholder", src:
|
|
119
|
+
result: (_jsx("div", { className: "rcm-image-field-cell", children: _jsx("img", { className: "rcm-image-field-thumb rcm-image-field-thumb-placeholder", src: getEndpoint('noImageFallback'), alt: "no image" }) })),
|
|
119
120
|
};
|
|
120
121
|
})();
|
|
121
122
|
}
|
|
@@ -12,6 +12,7 @@ import { IconPhotoPlus, IconTrash } from '@tabler/icons-react';
|
|
|
12
12
|
import { MultipleAssetUpload } from './view/MultipleAssetUpload';
|
|
13
13
|
import { getAccessableAssetUrl } from '../../misc';
|
|
14
14
|
import { RegexLowerEnglishNumber } from '../../misc';
|
|
15
|
+
import { getEndpoint } from '../../config/RuntimeConfig';
|
|
15
16
|
import { isTrue } from '../../utils/BooleanUtil';
|
|
16
17
|
import { ViewHelpText } from '../form/ui/ViewHelpText';
|
|
17
18
|
import { Tooltip } from '../../ui';
|
|
@@ -135,7 +136,7 @@ const MultipleAssetFieldView = (props) => {
|
|
|
135
136
|
else {
|
|
136
137
|
const imgUrl = getAccessableAssetUrl(value.assets[index].url);
|
|
137
138
|
return (_jsx("img", { className: "rcm-asset-img", alt: `${value?.assets?.[index]?.description ?? ''}`, onError: (event) => {
|
|
138
|
-
event.currentTarget.src = '
|
|
139
|
+
event.currentTarget.src = getEndpoint('noImageFallback');
|
|
139
140
|
}, src: `${imgUrl}` }));
|
|
140
141
|
}
|
|
141
142
|
})() }) }) }, `td-${index}`) }) })] }) }, `asset${index}`));
|
|
@@ -4,16 +4,24 @@ import { Validation } from '../../validations/Validation';
|
|
|
4
4
|
import { FieldRenderParameters } from '../../config/EntityField';
|
|
5
5
|
import { RenderType } from '../../config/Config';
|
|
6
6
|
import { EntityForm } from '../../config/EntityForm';
|
|
7
|
+
import type { Session } from '../../auth/types';
|
|
7
8
|
interface PhoneNumberFieldProps extends ListableFormFieldProps {
|
|
8
9
|
enableSms?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export declare class PhoneNumberField extends ListableFormField<PhoneNumberField> {
|
|
11
12
|
enableSms?: boolean;
|
|
13
|
+
private smsPermissionOverride?;
|
|
12
14
|
constructor(name: string, order: number, validations?: Validation[], enableSms?: boolean);
|
|
13
15
|
/**
|
|
14
16
|
* Enable SMS functionality
|
|
15
17
|
*/
|
|
16
18
|
withSms(enabled?: boolean): this;
|
|
19
|
+
/**
|
|
20
|
+
* Override the SMS send permission predicate for this field instance.
|
|
21
|
+
* If not set, RuntimeConfig.permissions.canSendSms is used (library default: always true).
|
|
22
|
+
*/
|
|
23
|
+
withSmsPermission(predicate: (session?: Session) => boolean): this;
|
|
24
|
+
private resolveCanSendSms;
|
|
17
25
|
/**
|
|
18
26
|
* PhoneNumberField 핵심 렌더링 로직
|
|
19
27
|
*/
|
|
@@ -7,6 +7,7 @@ import { getInputRendererParameters } from '../helper/FieldRendererHelper';
|
|
|
7
7
|
import { formatPhoneNumber, removePhoneNumberHyphens } from '../../utils/PhoneUtil';
|
|
8
8
|
import { PhoneNumberFieldView } from './view/PhoneNumberFieldView';
|
|
9
9
|
import { PhoneNumberListView } from './view/PhoneNumberListView';
|
|
10
|
+
import { getPermission } from '../../config/RuntimeConfig';
|
|
10
11
|
export class PhoneNumberField extends ListableFormField {
|
|
11
12
|
constructor(name, order, validations, enableSms) {
|
|
12
13
|
super(name, order, 'phone');
|
|
@@ -21,6 +22,18 @@ export class PhoneNumberField extends ListableFormField {
|
|
|
21
22
|
this.enableSms = enabled;
|
|
22
23
|
return this;
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Override the SMS send permission predicate for this field instance.
|
|
27
|
+
* If not set, RuntimeConfig.permissions.canSendSms is used (library default: always true).
|
|
28
|
+
*/
|
|
29
|
+
withSmsPermission(predicate) {
|
|
30
|
+
this.smsPermissionOverride = predicate;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
resolveCanSendSms(session) {
|
|
34
|
+
const fn = this.smsPermissionOverride ?? getPermission('canSendSms');
|
|
35
|
+
return fn(session);
|
|
36
|
+
}
|
|
24
37
|
/**
|
|
25
38
|
* PhoneNumberField 핵심 렌더링 로직
|
|
26
39
|
*/
|
|
@@ -38,7 +51,7 @@ export class PhoneNumberField extends ListableFormField {
|
|
|
38
51
|
};
|
|
39
52
|
}
|
|
40
53
|
}
|
|
41
|
-
return (_jsx(PhoneNumberFieldView, { name: inputParams.name, value: inputParams.value, onChange: inputParams.onChange, onError: inputParams.onError, readonly: inputParams.readonly, placeHolder: inputParams.placeHolder, regex: regex, enableSms: this.enableSms, session: params.entityForm.session, renderType: params.entityForm.getRenderType() }));
|
|
54
|
+
return (_jsx(PhoneNumberFieldView, { name: inputParams.name, value: inputParams.value, onChange: inputParams.onChange, onError: inputParams.onError, readonly: inputParams.readonly, placeHolder: inputParams.placeHolder, regex: regex, enableSms: this.enableSms, session: params.entityForm.session, renderType: params.entityForm.getRenderType(), canSendSmsByPermission: this.resolveCanSendSms(params.entityForm.session) }));
|
|
42
55
|
})();
|
|
43
56
|
}
|
|
44
57
|
/**
|
|
@@ -72,7 +85,7 @@ export class PhoneNumberField extends ListableFormField {
|
|
|
72
85
|
// If SMS is enabled and we have a value, use PhoneNumberListView
|
|
73
86
|
if (this.enableSms && value) {
|
|
74
87
|
return Promise.resolve({
|
|
75
|
-
result: (_jsx(PhoneNumberListView, { phoneNumber: value, formattedValue: formatted, enableSms: this.enableSms, session: props.entityForm.session })),
|
|
88
|
+
result: (_jsx(PhoneNumberListView, { phoneNumber: value, formattedValue: formatted, enableSms: this.enableSms, session: props.entityForm.session, canSendSmsByPermission: this.resolveCanSendSms(props.entityForm.session) })),
|
|
76
89
|
linkOnCell: true, // Prevent row click from triggering when clicking the dropdown
|
|
77
90
|
});
|
|
78
91
|
}
|
|
@@ -82,7 +95,11 @@ export class PhoneNumberField extends ListableFormField {
|
|
|
82
95
|
* PhoneNumberField 인스턴스 생성
|
|
83
96
|
*/
|
|
84
97
|
createInstance(name, order) {
|
|
85
|
-
|
|
98
|
+
const cloned = new PhoneNumberField(name, order, this.validations, this.enableSms);
|
|
99
|
+
if (this.smsPermissionOverride) {
|
|
100
|
+
cloned.smsPermissionOverride = this.smsPermissionOverride;
|
|
101
|
+
}
|
|
102
|
+
return cloned;
|
|
86
103
|
}
|
|
87
104
|
static create(props) {
|
|
88
105
|
const field = new PhoneNumberField(props.name, props.order, props.validations, props.enableSms);
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
'use client';
|
|
3
3
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
4
|
-
import {
|
|
4
|
+
import { removeAssetServerPrefix } from '../../../misc';
|
|
5
5
|
import { getTranslation } from '../../../utils/i18n';
|
|
6
6
|
import { FileFieldValue, FileUploadInput } from '../../../ui';
|
|
7
7
|
import { useEffect, useState } from 'react';
|
|
8
|
-
const assetProcessUrl = '/asset/upload-file';
|
|
9
|
-
const assetServerUrl = ASSET_SERVER_URL;
|
|
10
|
-
const ASSET_PREFIX = '/static-resource/';
|
|
11
8
|
export const MultipleAssetUpload = (props) => {
|
|
12
9
|
const maxFiles = 1;
|
|
13
10
|
const [fileValue, setFileValue] = useState(FileFieldValue.create());
|
|
@@ -14,6 +14,8 @@ interface PhoneNumberFieldViewProps {
|
|
|
14
14
|
enableSms?: boolean | undefined;
|
|
15
15
|
session?: Session | undefined;
|
|
16
16
|
renderType?: RenderType | undefined;
|
|
17
|
+
/** Permission to send SMS, resolved by PhoneNumberField at render time. */
|
|
18
|
+
canSendSmsByPermission?: boolean | undefined;
|
|
17
19
|
}
|
|
18
|
-
export declare const PhoneNumberFieldView: ({ name, value, onChange, onError, readonly, placeHolder, regex, enableSms, session, renderType, }: PhoneNumberFieldViewProps) => import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export declare const PhoneNumberFieldView: ({ name, value, onChange, onError, readonly, placeHolder, regex, enableSms, session, renderType, canSendSmsByPermission, }: PhoneNumberFieldViewProps) => import("react/jsx-runtime").JSX.Element;
|
|
19
21
|
export {};
|
|
@@ -8,7 +8,7 @@ import { showToast } from '../../../message';
|
|
|
8
8
|
import { readonlyClass } from '../../../ui';
|
|
9
9
|
import { formatPhoneNumber, removePhoneNumberHyphens } from '../../../utils/PhoneUtil';
|
|
10
10
|
import { SmsModal } from './SmsModal';
|
|
11
|
-
export const PhoneNumberFieldView = ({ name, value, onChange, onError, readonly = false, placeHolder, regex, enableSms, session, renderType, }) => {
|
|
11
|
+
export const PhoneNumberFieldView = ({ name, value, onChange, onError, readonly = false, placeHolder, regex, enableSms, session, renderType, canSendSmsByPermission, }) => {
|
|
12
12
|
const { openModal, closeModal } = useModalManagerStore();
|
|
13
13
|
const [displayValue, setDisplayValue] = useState('');
|
|
14
14
|
// Sync displayValue when external value changes
|
|
@@ -21,11 +21,10 @@ export const PhoneNumberFieldView = ({ name, value, onChange, onError, readonly
|
|
|
21
21
|
setDisplayValue('');
|
|
22
22
|
}
|
|
23
23
|
}, [value]);
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const canSendSms = isAdmin && enableSms && displayValue && renderType === 'update';
|
|
24
|
+
// SMS can be sent if: permitted + enableSms + phoneNumber + update mode
|
|
25
|
+
// `canSendSmsByPermission` is evaluated and injected by PhoneNumberField
|
|
26
|
+
// (either field-level withSmsPermission override or RuntimeConfig.permissions.canSendSms).
|
|
27
|
+
const canSendSms = canSendSmsByPermission && enableSms && displayValue && renderType === 'update';
|
|
29
28
|
// Copy is always available when there's a phone number
|
|
30
29
|
const canCopy = !!displayValue;
|
|
31
30
|
const handleChange = (e) => {
|
|
@@ -4,6 +4,8 @@ interface PhoneNumberListViewProps {
|
|
|
4
4
|
formattedValue: string;
|
|
5
5
|
enableSms?: boolean | undefined;
|
|
6
6
|
session?: Session | undefined;
|
|
7
|
+
/** Permission to send SMS, resolved by PhoneNumberField at render time. */
|
|
8
|
+
canSendSmsByPermission?: boolean | undefined;
|
|
7
9
|
}
|
|
8
|
-
export declare const PhoneNumberListView: ({ phoneNumber, formattedValue, enableSms,
|
|
10
|
+
export declare const PhoneNumberListView: ({ phoneNumber, formattedValue, enableSms, canSendSmsByPermission, }: PhoneNumberListViewProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
11
|
export {};
|
|
@@ -7,13 +7,11 @@ import { getOverlayZIndex, POPOVER_Z_INDEX, useModalManagerStore } from '../../.
|
|
|
7
7
|
import { showToast } from '../../../message';
|
|
8
8
|
import { SmsModal } from './SmsModal';
|
|
9
9
|
import { formatPhoneNumber } from '../../../utils/PhoneUtil';
|
|
10
|
-
export const PhoneNumberListView = ({ phoneNumber, formattedValue, enableSms,
|
|
10
|
+
export const PhoneNumberListView = ({ phoneNumber, formattedValue, enableSms, canSendSmsByPermission, }) => {
|
|
11
11
|
const { openModal, closeModal } = useModalManagerStore();
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
// SMS can be sent if: admin + enableSms + phoneNumber
|
|
16
|
-
const canSendSms = isAdmin && enableSms && phoneNumber;
|
|
12
|
+
// SMS can be sent if: permitted + enableSms + phoneNumber
|
|
13
|
+
// `canSendSmsByPermission` is evaluated and injected by PhoneNumberField.
|
|
14
|
+
const canSendSms = canSendSmsByPermission && enableSms && phoneNumber;
|
|
17
15
|
const handleCopy = async (e) => {
|
|
18
16
|
e.stopPropagation();
|
|
19
17
|
try {
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { RequestUtil } from '../../../misc';
|
|
5
5
|
import { showAlert, showSuccess } from '../../../message';
|
|
6
|
+
import { getEndpoint } from '../../../config/RuntimeConfig';
|
|
6
7
|
export const SmsModal = ({ phoneNumber, onClose }) => {
|
|
7
8
|
const [senderAddress, setSenderAddress] = useState('');
|
|
8
9
|
const [content, setContent] = useState('');
|
|
@@ -34,7 +35,7 @@ export const SmsModal = ({ phoneNumber, onClose }) => {
|
|
|
34
35
|
setLoadingSenderList(true);
|
|
35
36
|
try {
|
|
36
37
|
const response = await RequestUtil.getExternalApiDataWithError({
|
|
37
|
-
url: '
|
|
38
|
+
url: getEndpoint('smsSenderList'),
|
|
38
39
|
method: 'GET',
|
|
39
40
|
});
|
|
40
41
|
const senderCache = response.data;
|
|
@@ -86,7 +87,7 @@ export const SmsModal = ({ phoneNumber, onClose }) => {
|
|
|
86
87
|
toList: [{ address: phoneNumber }],
|
|
87
88
|
};
|
|
88
89
|
const response = await RequestUtil.getExternalApiDataWithError({
|
|
89
|
-
url: '
|
|
90
|
+
url: getEndpoint('smsNotificationSend'),
|
|
90
91
|
method: 'POST',
|
|
91
92
|
formData: notificationQueue,
|
|
92
93
|
});
|
|
@@ -27,7 +27,7 @@ export function getOverwriteButton(buttons, id) {
|
|
|
27
27
|
* @param props
|
|
28
28
|
*/
|
|
29
29
|
export async function getEntityFormButtons(props) {
|
|
30
|
-
const { router, pathname, entityForm, setErrors, setNotifications, postSave, useCreateStep, showModal, closeModal, closeTopModal, getModalData, updateModalData, } = props;
|
|
30
|
+
const { router, pathname, entityForm, setEntityForm, setErrors, setNotifications, postSave, useCreateStep, showModal, closeModal, closeTopModal, getModalData, updateModalData, } = props;
|
|
31
31
|
if (!entityForm)
|
|
32
32
|
return [];
|
|
33
33
|
const readonly = isTrue(props.readonly) || isTrue(entityForm?.readonly);
|
|
@@ -146,6 +146,7 @@ export async function getEntityFormButtons(props) {
|
|
|
146
146
|
pathname,
|
|
147
147
|
setErrors,
|
|
148
148
|
setNotifications,
|
|
149
|
+
...(setEntityForm !== undefined ? { setEntityForm } : {}),
|
|
149
150
|
...(useCreateStep && {
|
|
150
151
|
step: {
|
|
151
152
|
useCreateStep: true,
|
|
@@ -172,9 +173,20 @@ export async function getEntityFormButtons(props) {
|
|
|
172
173
|
if (button.onClick !== undefined) {
|
|
173
174
|
const form = await button.onClick(buttonProps);
|
|
174
175
|
if (form.errors && form.errors.length > 0) {
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
// Propagate the returned form so ViewEntityFormErrors picks
|
|
177
|
+
// up field-level errors (entityForm.getErrorMap()), matching
|
|
178
|
+
// the behaviour of the built-in SaveButton.
|
|
179
|
+
setEntityForm?.(form);
|
|
180
|
+
// entityForm.getErrorMap() 이 채워지면 ShowNotifications 의 띠는
|
|
181
|
+
// 표시되지 않으므로(필드별로 표시), 여기에서는 매핑 가능한 필드가
|
|
182
|
+
// 하나도 없을 때만 string 메시지를 fallback 으로 노출한다.
|
|
183
|
+
if (form.getErrorMap().size === 0) {
|
|
184
|
+
const errorMessages = form.errors.flatMap((error) => error.errors);
|
|
185
|
+
setErrors(errorMessages);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
setErrors([]);
|
|
189
|
+
}
|
|
178
190
|
return;
|
|
179
191
|
}
|
|
180
192
|
else {
|
|
@@ -17,7 +17,8 @@ import { useListGridLogic } from './hooks/useListGridLogic';
|
|
|
17
17
|
import { ShowNotifications } from '../helper/ShowNotifications';
|
|
18
18
|
import { Stack } from '../../ui';
|
|
19
19
|
import { SubCollectionViewModal } from './ui/SubCollectionViewModal';
|
|
20
|
-
import {
|
|
20
|
+
import { useSession } from '../../auth';
|
|
21
|
+
import { getPermission } from '../../config/RuntimeConfig';
|
|
21
22
|
import { perfLog } from './utils/performanceLogger';
|
|
22
23
|
import { getListGridThemeByVariant, ListGridThemeProvider, useListGridTheme, } from './context/ListGridThemeContext';
|
|
23
24
|
import { EntityFormScopeProvider, useEntityFormScope } from './context/EntityFormScopeContext';
|
|
@@ -96,13 +97,14 @@ export const ViewListGrid = (props) => {
|
|
|
96
97
|
// Hook은 조건문 전에 호출되어야 함
|
|
97
98
|
const sessionFromHook = useSession();
|
|
98
99
|
const session = props.session ?? entityForm.getSession() ?? sessionFromHook;
|
|
99
|
-
//
|
|
100
|
-
//
|
|
100
|
+
// "새 창 열기" 버튼 표시 권한.
|
|
101
|
+
// 라이브러리 자체는 role 을 모르며, RuntimeConfig.permissions.canOpenInNewWindow 에
|
|
102
|
+
// 주입된 predicate 가 판정한다. 호스트가 설정하지 않으면 항상 true.
|
|
103
|
+
//
|
|
104
|
+
// TODO: ListGrid 단위 override (withOpenInNewWindowPermission) 를 추가할 수도 있지만,
|
|
105
|
+
// 현재는 전역 predicate 만 사용한다 — 필요 시 listGrid prop 으로 확장.
|
|
101
106
|
const isAdmin = React.useMemo(() => {
|
|
102
|
-
|
|
103
|
-
return hasAnyRole(session, 'ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_STAFF');
|
|
104
|
-
}
|
|
105
|
-
return false;
|
|
107
|
+
return getPermission('canOpenInNewWindow')(session ?? undefined);
|
|
106
108
|
}, [session]);
|
|
107
109
|
// QuickSearch 필드명 Set 생성 (헤더 필터 비활성화용)
|
|
108
110
|
// Note: 이 hook은 early return 이전에 있어야 hooks 순서가 일정함
|
|
@@ -4,8 +4,12 @@ import { FieldRenderParameters, FilterRenderParameters } from '../../config/Enti
|
|
|
4
4
|
interface RevisionFieldProps extends FormFieldProps {
|
|
5
5
|
}
|
|
6
6
|
export declare class RevisionField extends FormField<RevisionField> {
|
|
7
|
+
private apiUrlOverride?;
|
|
7
8
|
constructor(name: string, order: number);
|
|
8
9
|
protected createInstance(name: string, order: number): RevisionField;
|
|
10
|
+
/** Override the revision API URL for this field instance. */
|
|
11
|
+
withApiUrl(url: string): this;
|
|
12
|
+
getApiUrl(): string;
|
|
9
13
|
protected renderInstance(params: FieldRenderParameters): Promise<React.ReactNode | null>;
|
|
10
14
|
protected renderListFilterInstance(params: FilterRenderParameters): Promise<React.ReactNode | null>;
|
|
11
15
|
static create(props: RevisionFieldProps): RevisionField;
|
|
@@ -11,7 +11,7 @@ import { IconHistory } from '@tabler/icons-react';
|
|
|
11
11
|
import { fDateTime } from '../../misc';
|
|
12
12
|
import { Pagination } from '../../ui';
|
|
13
13
|
import { getTranslation } from '../../utils/i18n';
|
|
14
|
-
|
|
14
|
+
import { getEndpoint } from '../../config/RuntimeConfig';
|
|
15
15
|
// Audit/timestamp fields excluded from diff (always change on every update)
|
|
16
16
|
const AUDIT_FIELD_NAMES = new Set([
|
|
17
17
|
'updatedAt',
|
|
@@ -66,6 +66,14 @@ export class RevisionField extends FormField {
|
|
|
66
66
|
createInstance(name, order) {
|
|
67
67
|
return new RevisionField(name, order);
|
|
68
68
|
}
|
|
69
|
+
/** Override the revision API URL for this field instance. */
|
|
70
|
+
withApiUrl(url) {
|
|
71
|
+
this.apiUrlOverride = url;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
getApiUrl() {
|
|
75
|
+
return this.apiUrlOverride ?? getEndpoint('revisionApi');
|
|
76
|
+
}
|
|
69
77
|
async renderInstance(params) {
|
|
70
78
|
const { entityForm, subCollectionEntity } = params;
|
|
71
79
|
// subCollectionEntity인 경우 렌더링하지 않음
|
|
@@ -76,7 +84,7 @@ export class RevisionField extends FormField {
|
|
|
76
84
|
if (entityForm.getRenderType() === 'create') {
|
|
77
85
|
return null;
|
|
78
86
|
}
|
|
79
|
-
return _jsx(RevisionFieldRenderer, { entityForm: entityForm });
|
|
87
|
+
return _jsx(RevisionFieldRenderer, { entityForm: entityForm, apiUrl: this.getApiUrl() });
|
|
80
88
|
}
|
|
81
89
|
renderListFilterInstance(params) {
|
|
82
90
|
// 리스트 필터에서는 지원하지 않음
|
|
@@ -86,7 +94,7 @@ export class RevisionField extends FormField {
|
|
|
86
94
|
return new RevisionField(props.name, props.order).copyFields(props, true);
|
|
87
95
|
}
|
|
88
96
|
}
|
|
89
|
-
const RevisionFieldRenderer = ({ entityForm }) => {
|
|
97
|
+
const RevisionFieldRenderer = ({ entityForm, apiUrl }) => {
|
|
90
98
|
const [revisions, setRevisions] = useState([]);
|
|
91
99
|
const [loading, setLoading] = useState(false);
|
|
92
100
|
const [totalPage, setTotalPage] = useState(0);
|
|
@@ -108,7 +116,7 @@ const RevisionFieldRenderer = ({ entityForm }) => {
|
|
|
108
116
|
.withSort('createdAt', 'DESC')
|
|
109
117
|
.withPage(page)
|
|
110
118
|
.withPageSize(10);
|
|
111
|
-
const searchResult = await PageResult.fetchListData(
|
|
119
|
+
const searchResult = await PageResult.fetchListData(apiUrl, searchForm);
|
|
112
120
|
if (searchResult && searchResult.list) {
|
|
113
121
|
setRevisions(searchResult.list);
|
|
114
122
|
setTotalPage(searchResult.totalPage);
|
|
@@ -8,8 +8,7 @@ import { delay, entityErrorToString } from './EntityFormMethod';
|
|
|
8
8
|
import { EntityFormExtensions } from './form/EntityFormExtensions';
|
|
9
9
|
import { ExtensionPoint } from '../extensions/EntityFormExtension.types';
|
|
10
10
|
import { PhoneNumberField } from '../components/fields/PhoneNumberField';
|
|
11
|
-
import { createSmsHistoryField } from '../extensions/FieldExtensions';
|
|
12
|
-
import { hasAnyRole } from '../auth';
|
|
11
|
+
import { createSmsHistoryField, getPhoneNumberSmsHistoryInjectConfig, } from '../extensions/FieldExtensions';
|
|
13
12
|
export class EntityForm extends EntityFormExtensions {
|
|
14
13
|
constructor(name, url) {
|
|
15
14
|
super(name, url);
|
|
@@ -169,21 +168,23 @@ export class EntityForm extends EntityFormExtensions {
|
|
|
169
168
|
const newField = field.withOptions(options).clone(true);
|
|
170
169
|
entityForm.fields.set(field.getName(), newField);
|
|
171
170
|
}
|
|
172
|
-
// PhoneNumberField
|
|
171
|
+
// PhoneNumberField 자동 주입: 호스트 앱이 registerPhoneNumberSmsHistoryInject 로
|
|
172
|
+
// 활성화한 경우에만 SMS 발송 이력 탭을 추가한다. 권한 판정은 호스트가 주입한
|
|
173
|
+
// predicate 를 사용하며, 라이브러리 자체는 role 문자열을 알지 않는다.
|
|
173
174
|
if (field instanceof PhoneNumberField) {
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
const injectConfig = getPhoneNumberSmsHistoryInjectConfig();
|
|
176
|
+
if (injectConfig.enabled &&
|
|
177
|
+
isTrue(field.enableSms) &&
|
|
178
|
+
injectConfig.permission(entityForm.session)) {
|
|
178
179
|
const SMS_HISTORY_TAB = {
|
|
179
|
-
id:
|
|
180
|
-
label:
|
|
181
|
-
order: STATUS_TAB_INFO.order
|
|
180
|
+
id: injectConfig.tabId,
|
|
181
|
+
label: injectConfig.tabLabel,
|
|
182
|
+
order: STATUS_TAB_INFO.order + injectConfig.tabOrderOffset,
|
|
182
183
|
hidden: false,
|
|
183
184
|
};
|
|
184
185
|
const smsHistoryField = createSmsHistoryField(field.getName() + 'SmsHistory', field.getOrder() + 1, field.getName());
|
|
185
186
|
if (smsHistoryField) {
|
|
186
|
-
smsHistoryField.withLabel(
|
|
187
|
+
smsHistoryField.withLabel(injectConfig.tabLabel).withModifyOnly().withHideLabel(true);
|
|
187
188
|
entityForm.addFields({
|
|
188
189
|
tab: SMS_HISTORY_TAB,
|
|
189
190
|
items: [smsHistoryField],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EntityForm } from '../config/EntityForm';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { Dispatch, ReactNode, SetStateAction } from 'react';
|
|
3
3
|
import { LabelType } from '../config/Config';
|
|
4
4
|
import { ModalOptions } from '../store';
|
|
5
5
|
import type { RouterApi } from '../router/types';
|
|
@@ -15,6 +15,13 @@ export interface EntityFormButtonProps {
|
|
|
15
15
|
pathname: string | null;
|
|
16
16
|
setErrors: (errors: string[]) => void;
|
|
17
17
|
setNotifications: (notifications: string[]) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Setter for the host EntityForm state. When a custom button's onClick
|
|
20
|
+
* mutates the form (e.g. via `entityForm.save()` returning a new instance
|
|
21
|
+
* with field errors), call this to propagate the new instance to
|
|
22
|
+
* ViewEntityForm so that ViewEntityFormErrors can render field-level errors.
|
|
23
|
+
*/
|
|
24
|
+
setEntityForm?: Dispatch<SetStateAction<EntityForm | undefined>>;
|
|
18
25
|
step?: EntityFormButtonStepInfo;
|
|
19
26
|
showModal?: (options: ModalOptions) => string;
|
|
20
27
|
closeModal?: (id: string) => Promise<void>;
|
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
import type { Session } from '../auth/types';
|
|
2
|
+
/**
|
|
3
|
+
* Named API endpoints used internally by built-in fields and features.
|
|
4
|
+
* Hosts override at bootstrap via `configureRuntime({ endpoints: { ... } })`.
|
|
5
|
+
* Individual fields can still override via their own chainable setters.
|
|
6
|
+
*/
|
|
7
|
+
export interface ListGridEndpoints {
|
|
8
|
+
/** Excel import suffix appended to an entity's base URL. */
|
|
9
|
+
excelUpload: string;
|
|
10
|
+
/** Excel download history logging endpoint. */
|
|
11
|
+
excelDownloadHistory: string;
|
|
12
|
+
/** Custom option single-alias fetch. */
|
|
13
|
+
customOptionByAlias: string;
|
|
14
|
+
/** Custom option bulk-alias fetch. */
|
|
15
|
+
customOptionByAliases: string;
|
|
16
|
+
/** Asset upload endpoint (MultipleAssetField, etc.). */
|
|
17
|
+
assetUpload: string;
|
|
18
|
+
/** Static resource prefix for asset URLs. */
|
|
19
|
+
staticResourcePrefix: string;
|
|
20
|
+
/** SMS sender list endpoint (SMS modal). */
|
|
21
|
+
smsSenderList: string;
|
|
22
|
+
/** SMS send endpoint. */
|
|
23
|
+
smsNotificationSend: string;
|
|
24
|
+
/** Revision history endpoint. */
|
|
25
|
+
revisionApi: string;
|
|
26
|
+
/** Fallback image URL when an asset cannot be loaded. */
|
|
27
|
+
noImageFallback: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Permission predicate hooks. Each returns `true` when the UI affordance
|
|
31
|
+
* is allowed for the given session. Library defaults are permissive
|
|
32
|
+
* (`() => true`); hosts install their own role checks at bootstrap.
|
|
33
|
+
*/
|
|
34
|
+
export interface ListGridPermissions {
|
|
35
|
+
/** PhoneNumberField "Send SMS" button visibility. */
|
|
36
|
+
canSendSms: (session?: Session) => boolean;
|
|
37
|
+
/** ListGrid "Open in new window" button visibility. */
|
|
38
|
+
canOpenInNewWindow: (session?: Session) => boolean;
|
|
39
|
+
}
|
|
1
40
|
export interface RuntimeConfig {
|
|
2
41
|
/** Enables server-side-rendered list caching hints. */
|
|
3
42
|
cacheControl?: boolean;
|
|
@@ -13,6 +52,24 @@ export interface RuntimeConfig {
|
|
|
13
52
|
kakaoMapAppKey?: string;
|
|
14
53
|
/** simpleCrypt passphrase / secret (replaces NEXT_PUBLIC_CRYPT_KEY). */
|
|
15
54
|
cryptKey?: string;
|
|
55
|
+
/** Named API endpoints; hosts override any subset. */
|
|
56
|
+
endpoints?: Partial<ListGridEndpoints>;
|
|
57
|
+
/** Permission predicates; hosts override any subset. */
|
|
58
|
+
permissions?: Partial<ListGridPermissions>;
|
|
59
|
+
}
|
|
60
|
+
interface ResolvedRuntimeConfig {
|
|
61
|
+
cacheControl: boolean;
|
|
62
|
+
useServerSideCache: boolean;
|
|
63
|
+
searchFormHashKey: string;
|
|
64
|
+
debugListGridPerformance: boolean;
|
|
65
|
+
isDevelopment: boolean;
|
|
66
|
+
kakaoMapAppKey: string;
|
|
67
|
+
cryptKey: string;
|
|
68
|
+
endpoints: ListGridEndpoints;
|
|
69
|
+
permissions: ListGridPermissions;
|
|
16
70
|
}
|
|
17
71
|
export declare function configureRuntime(config: RuntimeConfig): void;
|
|
18
|
-
export declare function getRuntimeConfig():
|
|
72
|
+
export declare function getRuntimeConfig(): ResolvedRuntimeConfig;
|
|
73
|
+
export declare function getEndpoint(name: keyof ListGridEndpoints): string;
|
|
74
|
+
export declare function getPermission(name: keyof ListGridPermissions): (session?: Session) => boolean;
|
|
75
|
+
export {};
|
|
@@ -4,6 +4,28 @@
|
|
|
4
4
|
// framework-agnostic registry. Host apps call `configureRuntime({...})`
|
|
5
5
|
// at bootstrap with their platform-specific values (from Next env vars,
|
|
6
6
|
// Vite import.meta.env, custom config, etc.).
|
|
7
|
+
//
|
|
8
|
+
// Stage 9 (0.3.0) — host-coupling detox.
|
|
9
|
+
// Two registries were added to replace hardcoded project literals:
|
|
10
|
+
// - `endpoints`: named API paths used by built-in features
|
|
11
|
+
// - `permissions`: predicate hooks for UI permission gates
|
|
12
|
+
// See docs/REFACTOR_HOST_COUPLING.md for rationale.
|
|
13
|
+
const DEFAULT_ENDPOINTS = {
|
|
14
|
+
excelUpload: '/excel-upload',
|
|
15
|
+
excelDownloadHistory: '/excel-download-history/add',
|
|
16
|
+
customOptionByAlias: '/option/by-alias',
|
|
17
|
+
customOptionByAliases: '/option/by-aliases',
|
|
18
|
+
assetUpload: '/asset/upload-file',
|
|
19
|
+
staticResourcePrefix: '/static-resource/',
|
|
20
|
+
smsSenderList: '/sms-sender/list',
|
|
21
|
+
smsNotificationSend: '/notification/send',
|
|
22
|
+
revisionApi: '/revision',
|
|
23
|
+
noImageFallback: '/assets/images/no-image.png',
|
|
24
|
+
};
|
|
25
|
+
const DEFAULT_PERMISSIONS = {
|
|
26
|
+
canSendSms: () => true,
|
|
27
|
+
canOpenInNewWindow: () => true,
|
|
28
|
+
};
|
|
7
29
|
const DEFAULT = {
|
|
8
30
|
cacheControl: false,
|
|
9
31
|
useServerSideCache: false,
|
|
@@ -12,11 +34,29 @@ const DEFAULT = {
|
|
|
12
34
|
isDevelopment: false,
|
|
13
35
|
kakaoMapAppKey: '',
|
|
14
36
|
cryptKey: '',
|
|
37
|
+
endpoints: { ...DEFAULT_ENDPOINTS },
|
|
38
|
+
permissions: { ...DEFAULT_PERMISSIONS },
|
|
39
|
+
};
|
|
40
|
+
let _config = {
|
|
41
|
+
...DEFAULT,
|
|
42
|
+
endpoints: { ...DEFAULT_ENDPOINTS },
|
|
43
|
+
permissions: { ...DEFAULT_PERMISSIONS },
|
|
15
44
|
};
|
|
16
|
-
let _config = { ...DEFAULT };
|
|
17
45
|
export function configureRuntime(config) {
|
|
18
|
-
|
|
46
|
+
const { endpoints: epOverride, permissions: pmOverride, ...scalarOverrides } = config;
|
|
47
|
+
_config = {
|
|
48
|
+
..._config,
|
|
49
|
+
...scalarOverrides,
|
|
50
|
+
endpoints: { ..._config.endpoints, ...(epOverride ?? {}) },
|
|
51
|
+
permissions: { ..._config.permissions, ...(pmOverride ?? {}) },
|
|
52
|
+
};
|
|
19
53
|
}
|
|
20
54
|
export function getRuntimeConfig() {
|
|
21
55
|
return _config;
|
|
22
56
|
}
|
|
57
|
+
export function getEndpoint(name) {
|
|
58
|
+
return _config.endpoints[name];
|
|
59
|
+
}
|
|
60
|
+
export function getPermission(name) {
|
|
61
|
+
return _config.permissions[name];
|
|
62
|
+
}
|
|
@@ -1,5 +1,30 @@
|
|
|
1
|
+
import type { Session } from '../auth/types';
|
|
1
2
|
export interface SmsHistoryFieldConstructor {
|
|
2
3
|
new (fieldName: string, order: number, targetFieldName: string): any;
|
|
3
4
|
}
|
|
4
5
|
export declare function registerSmsHistoryField(ctor: SmsHistoryFieldConstructor): void;
|
|
5
6
|
export declare function createSmsHistoryField(fieldName: string, order: number, targetFieldName: string): any | null;
|
|
7
|
+
/**
|
|
8
|
+
* Auto-injection configuration for the SMS history tab.
|
|
9
|
+
*
|
|
10
|
+
* When a `PhoneNumberField` with `enableSms=true` is processed during EntityForm
|
|
11
|
+
* initialization and this injector is registered with `enabled: true`, the
|
|
12
|
+
* library adds a "SMS 발송 이력" tab containing an `SmsHistoryField` (obtained
|
|
13
|
+
* via `createSmsHistoryField`) next to the phone number field.
|
|
14
|
+
*
|
|
15
|
+
* The `permission` predicate gates the injection per user session. If unset,
|
|
16
|
+
* the tab is injected for every session (library default: permissive).
|
|
17
|
+
*
|
|
18
|
+
* Default: disabled (opt-in). Hosts must explicitly call
|
|
19
|
+
* `registerPhoneNumberSmsHistoryInject({ enabled: true, ... })` at bootstrap.
|
|
20
|
+
*/
|
|
21
|
+
export interface PhoneNumberSmsHistoryInjectConfig {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
permission?: (session?: Session) => boolean;
|
|
24
|
+
tabLabel?: string;
|
|
25
|
+
tabId?: string;
|
|
26
|
+
/** Offset applied to STATUS_TAB_INFO.order to position the tab. Default: -10. */
|
|
27
|
+
tabOrderOffset?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function registerPhoneNumberSmsHistoryInject(config: PhoneNumberSmsHistoryInjectConfig): void;
|
|
30
|
+
export declare function getPhoneNumberSmsHistoryInjectConfig(): Required<PhoneNumberSmsHistoryInjectConfig>;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Host-supplied domain field extension registry.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// `@rchemist/listgrid` does not ship a built-in `SmsHistoryField` implementation
|
|
4
|
+
// because the field depends on a host application's SMS history schema.
|
|
5
|
+
// Host apps register their own implementation via `registerSmsHistoryField`
|
|
6
|
+
// (and optionally opt into the auto-injection behaviour via
|
|
7
|
+
// `registerPhoneNumberSmsHistoryInject`).
|
|
7
8
|
//
|
|
8
9
|
// If no host implementation is registered, the SMS history behaviour is
|
|
9
|
-
// silently skipped — callers
|
|
10
|
+
// silently skipped — callers check `createSmsHistoryField` for null.
|
|
10
11
|
let _smsHistoryFieldCtor;
|
|
11
12
|
export function registerSmsHistoryField(ctor) {
|
|
12
13
|
_smsHistoryFieldCtor = ctor;
|
|
@@ -19,3 +20,22 @@ export function createSmsHistoryField(fieldName, order, targetFieldName) {
|
|
|
19
20
|
}
|
|
20
21
|
return new _smsHistoryFieldCtor(fieldName, order, targetFieldName);
|
|
21
22
|
}
|
|
23
|
+
let _smsHistoryInjectConfig = {
|
|
24
|
+
enabled: false,
|
|
25
|
+
permission: () => true,
|
|
26
|
+
tabLabel: 'SMS 발송 이력',
|
|
27
|
+
tabId: 'smsHistory',
|
|
28
|
+
tabOrderOffset: -10,
|
|
29
|
+
};
|
|
30
|
+
export function registerPhoneNumberSmsHistoryInject(config) {
|
|
31
|
+
_smsHistoryInjectConfig = {
|
|
32
|
+
enabled: config.enabled,
|
|
33
|
+
permission: config.permission ?? (() => true),
|
|
34
|
+
tabLabel: config.tabLabel ?? 'SMS 발송 이력',
|
|
35
|
+
tabId: config.tabId ?? 'smsHistory',
|
|
36
|
+
tabOrderOffset: config.tabOrderOffset ?? -10,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function getPhoneNumberSmsHistoryInjectConfig() {
|
|
40
|
+
return _smsHistoryInjectConfig;
|
|
41
|
+
}
|
package/dist/listgrid/index.d.ts
CHANGED
|
@@ -11,9 +11,10 @@ export { configureLoading, useLoadingStore } from './loading';
|
|
|
11
11
|
export type { LoadingStore } from './loading';
|
|
12
12
|
export { useModalManagerStore, configureOverlayZIndex, getOverlayZIndex, POPOVER_Z_INDEX, } from './store';
|
|
13
13
|
export type { ModalOptions } from './store';
|
|
14
|
-
export { registerSmsHistoryField, createSmsHistoryField } from './extensions/FieldExtensions';
|
|
15
|
-
export {
|
|
16
|
-
export
|
|
14
|
+
export { registerSmsHistoryField, createSmsHistoryField, registerPhoneNumberSmsHistoryInject, getPhoneNumberSmsHistoryInjectConfig, } from './extensions/FieldExtensions';
|
|
15
|
+
export type { SmsHistoryFieldConstructor, PhoneNumberSmsHistoryInjectConfig, } from './extensions/FieldExtensions';
|
|
16
|
+
export { configureRuntime, getRuntimeConfig, getEndpoint, getPermission, } from './config/RuntimeConfig';
|
|
17
|
+
export type { RuntimeConfig, ListGridEndpoints, ListGridPermissions } from './config/RuntimeConfig';
|
|
17
18
|
export { configureTranslator, getTranslation } from './utils/i18n';
|
|
18
19
|
export type { Translator, TranslatorI18n, TranslatorFactory } from './utils/i18n';
|
|
19
20
|
export { registerMenuPermissionChecker, checkAdminMenuPermission, DEFAULT_MENU_ALIAS, } from './menu';
|
package/dist/listgrid/index.js
CHANGED
|
@@ -12,10 +12,12 @@ export { configureMessages, showAlert, showConfirm, showError, showSuccess, show
|
|
|
12
12
|
export { configureLoading, useLoadingStore } from './loading';
|
|
13
13
|
// Modal manager store (zustand-based).
|
|
14
14
|
export { useModalManagerStore, configureOverlayZIndex, getOverlayZIndex, POPOVER_Z_INDEX, } from './store';
|
|
15
|
-
// Field extension registry — host apps register domain-specific field classes
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
// Field extension registry — host apps register domain-specific field classes
|
|
16
|
+
// and opt into domain-specific auto-injection behaviours.
|
|
17
|
+
export { registerSmsHistoryField, createSmsHistoryField, registerPhoneNumberSmsHistoryInject, getPhoneNumberSmsHistoryInjectConfig, } from './extensions/FieldExtensions';
|
|
18
|
+
// Runtime configuration — replaces hard-coded process.env.NEXT_PUBLIC_* access
|
|
19
|
+
// and hosts the endpoint/permission registries for framework-agnostic wiring.
|
|
20
|
+
export { configureRuntime, getRuntimeConfig, getEndpoint, getPermission, } from './config/RuntimeConfig';
|
|
19
21
|
// i18n extension point — host injects a translator factory at bootstrap.
|
|
20
22
|
export { configureTranslator, getTranslation } from './utils/i18n';
|
|
21
23
|
// Menu permission checker — host apps register a real checker that decides
|
|
@@ -2,6 +2,7 @@ import { isTrue } from '../../utils/BooleanUtil';
|
|
|
2
2
|
import * as FileSaver from 'file-saver';
|
|
3
3
|
import * as XLSX from 'xlsx-js-style';
|
|
4
4
|
import { isDataRowSet } from '../Type';
|
|
5
|
+
import { getEndpoint } from '../../config/RuntimeConfig';
|
|
5
6
|
export async function logExcelDownload(usePassword, condition) {
|
|
6
7
|
try {
|
|
7
8
|
const url = typeof window !== 'undefined' ? window.location.pathname : '';
|
|
@@ -9,7 +10,7 @@ export async function logExcelDownload(usePassword, condition) {
|
|
|
9
10
|
const conditionStr = raw.substring(0, 2000);
|
|
10
11
|
const { callExternalHttpRequest } = await import('../../utils/RequestUtil');
|
|
11
12
|
await callExternalHttpRequest({
|
|
12
|
-
url: '
|
|
13
|
+
url: getEndpoint('excelDownloadHistory'),
|
|
13
14
|
method: 'POST',
|
|
14
15
|
formData: { url, condition: conditionStr, usePassword },
|
|
15
16
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getEndpoint } from '../config/RuntimeConfig';
|
|
1
2
|
import { fDate, fDateTime } from '../misc';
|
|
2
3
|
import { isTrue } from '../utils/BooleanUtil';
|
|
3
4
|
import { getPlainText } from '../ui';
|
|
@@ -91,8 +92,8 @@ export class DataTransferConfig {
|
|
|
91
92
|
this.export.url = url;
|
|
92
93
|
}
|
|
93
94
|
if (this.import.url === undefined) {
|
|
94
|
-
// 엑셀 업로드 기본 경로
|
|
95
|
-
this.import.url = url + '
|
|
95
|
+
// 엑셀 업로드 기본 경로 (RuntimeConfig.endpoints.excelUpload 로 오버라이드 가능)
|
|
96
|
+
this.import.url = url + getEndpoint('excelUpload');
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
isSupportExport() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rchemist/listgrid",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Framework-free React CRUD UI engine — primitive-based design system, data-attr theming, and a full list/form renderer for RCM-framework-style entity backends.",
|
|
6
6
|
"keywords": [
|