@rchemist/listgrid 0.3.2 → 0.3.4
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/FileField.js +35 -1
- package/dist/listgrid/components/fields/ImageField.js +41 -1
- package/dist/listgrid/index.d.ts +1 -1
- package/dist/listgrid/index.js +1 -1
- package/dist/listgrid/misc/index.d.ts +5 -0
- package/dist/listgrid/misc/index.js +25 -0
- package/dist/styles/components.css +4 -1
- package/dist/styles.css +4 -1
- package/package.json +1 -1
|
@@ -4,10 +4,29 @@ import { FileFieldValue } from '../../ui';
|
|
|
4
4
|
import { LazyFileUploadInput as FileUploadInput } from '../../ui';
|
|
5
5
|
import { getInputRendererParameters } from '../helper/FieldRendererHelper';
|
|
6
6
|
import { isEmpty } from '../../utils';
|
|
7
|
-
import { getAccessableAssetUrl } from '../../misc';
|
|
7
|
+
import { getAccessableAssetUrl, isExternalUrl } from '../../misc';
|
|
8
8
|
import { IconDeviceFloppy } from '@tabler/icons-react';
|
|
9
9
|
import { TextInput } from '../../ui';
|
|
10
10
|
import { isBlank as isBlankString } from '../../utils/StringUtil';
|
|
11
|
+
/**
|
|
12
|
+
* 다양한 형태의 필드 값(`FileFieldValue` 인스턴스 / POJO / 문자열)에서
|
|
13
|
+
* 외부 절대 URL(`http(s)://`) 을 추출한다. 외부 URL 이 아니면 `undefined`.
|
|
14
|
+
*/
|
|
15
|
+
function pickExternalUrl(value) {
|
|
16
|
+
if (!value)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
return isExternalUrl(value) ? value.trim() : undefined;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'object') {
|
|
22
|
+
const files = Array.isArray(value.existFiles) ? value.existFiles : [];
|
|
23
|
+
for (const f of files) {
|
|
24
|
+
if (f && typeof f.url === 'string' && isExternalUrl(f.url))
|
|
25
|
+
return f.url.trim();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
11
30
|
export class FileField extends ListableFormField {
|
|
12
31
|
constructor(name, order, config) {
|
|
13
32
|
super(name, order, 'file');
|
|
@@ -58,6 +77,12 @@ export class FileField extends ListableFormField {
|
|
|
58
77
|
*/
|
|
59
78
|
renderInstance(params) {
|
|
60
79
|
return (async () => {
|
|
80
|
+
// 외부 URL 우회: 값이 `http(s)://` 절대 URL 이면 자체 asset 서버를 거치지 않고
|
|
81
|
+
// 그대로 다운로드 링크로 표시. (교체 시에는 기존 삭제 후 신규 등록 흐름을 사용.)
|
|
82
|
+
const externalUrl = pickExternalUrl(await this.getCurrentValue(params.entityForm.getRenderType()));
|
|
83
|
+
if (externalUrl) {
|
|
84
|
+
return (_jsx("div", { className: "rcm-file-field-external", children: _jsxs("a", { href: externalUrl, target: "_blank", rel: "noreferrer", className: "rcm-file-field-link", children: [_jsx(IconDeviceFloppy, { className: "rcm-file-field-icon" }), _jsx("span", { className: "rcm-file-field-name", children: externalUrl })] }) }));
|
|
85
|
+
}
|
|
61
86
|
return (_jsx(FileUploadInput, { config: this.config, ...await getInputRendererParameters(this, params) }));
|
|
62
87
|
})();
|
|
63
88
|
}
|
|
@@ -82,6 +107,15 @@ export class FileField extends ListableFormField {
|
|
|
82
107
|
return (async () => {
|
|
83
108
|
const value = await props.item;
|
|
84
109
|
if (value[this.name]) {
|
|
110
|
+
// 값이 단순 외부 URL 문자열로 들어온 케이스 — 자체 asset 서버를 거치지 않고
|
|
111
|
+
// 그대로 다운로드 링크로 렌더링.
|
|
112
|
+
const externalUrl = pickExternalUrl(value[this.name]);
|
|
113
|
+
if (externalUrl) {
|
|
114
|
+
return {
|
|
115
|
+
result: (_jsx("div", { className: "rcm-file-field-cell", children: _jsx("div", { className: "rcm-file-field-inner", children: _jsxs("a", { href: externalUrl, target: "_blank", rel: "noreferrer", className: "rcm-file-field-link", children: [_jsx(IconDeviceFloppy, { className: "rcm-file-field-icon" }), _jsx("span", { className: "rcm-file-field-name", children: externalUrl })] }) }) })),
|
|
116
|
+
linkOnCell: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
85
119
|
const file = value[this.name];
|
|
86
120
|
if (!isEmpty(file.existFiles) && !isBlankString(file.existFiles[0]?.url)) {
|
|
87
121
|
const fileDownloadUrl = getAccessableAssetUrl(file.existFiles[0].url);
|
|
@@ -3,9 +3,28 @@ import { ListableFormField, } from './abstract';
|
|
|
3
3
|
import { LazyFileUploadInput as FileUploadInput } from '../../ui';
|
|
4
4
|
import { getInputRendererParameters } from '../helper/FieldRendererHelper';
|
|
5
5
|
import { isEmpty } from '../../utils';
|
|
6
|
-
import { getAccessableAssetUrl } from '../../misc';
|
|
6
|
+
import { getAccessableAssetUrl, isExternalUrl } from '../../misc';
|
|
7
7
|
import { TextInput } from '../../ui';
|
|
8
8
|
import { getEndpoint } from '../../config/RuntimeConfig';
|
|
9
|
+
/**
|
|
10
|
+
* 다양한 형태의 필드 값(`FileFieldValue` 인스턴스 / POJO / 문자열)에서
|
|
11
|
+
* 외부 절대 URL(`http(s)://`) 을 추출한다. 외부 URL 이 아니면 `undefined`.
|
|
12
|
+
*/
|
|
13
|
+
function pickExternalUrl(value) {
|
|
14
|
+
if (!value)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (typeof value === 'string') {
|
|
17
|
+
return isExternalUrl(value) ? value.trim() : undefined;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === 'object') {
|
|
20
|
+
const files = Array.isArray(value.existFiles) ? value.existFiles : [];
|
|
21
|
+
for (const f of files) {
|
|
22
|
+
if (f && typeof f.url === 'string' && isExternalUrl(f.url))
|
|
23
|
+
return f.url.trim();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
9
28
|
export class ImageField extends ListableFormField {
|
|
10
29
|
constructor(name, order, config) {
|
|
11
30
|
super(name, order, 'file');
|
|
@@ -79,6 +98,15 @@ export class ImageField extends ListableFormField {
|
|
|
79
98
|
config.maxCount = 1;
|
|
80
99
|
}
|
|
81
100
|
}
|
|
101
|
+
// 외부 URL 우회: 값이 `http(s)://` 절대 URL 이면 자체 asset 서버를 거치지 않고
|
|
102
|
+
// 그대로 이미지로 표시한다. (첨부 정책상 교체는 "기존 삭제 후 신규 등록" 흐름이므로
|
|
103
|
+
// 별도 교체 컨트롤 없이 표시만으로 충분.)
|
|
104
|
+
const externalUrl = pickExternalUrl(await this.getCurrentValue(params.entityForm.getRenderType()));
|
|
105
|
+
if (externalUrl) {
|
|
106
|
+
return (_jsx("div", { className: "rcm-image-field-external", children: _jsx("img", { className: "rcm-image-field-external-img", src: externalUrl, alt: "external image", onError: (event) => {
|
|
107
|
+
event.currentTarget.src = getEndpoint('noImageFallback');
|
|
108
|
+
} }) }));
|
|
109
|
+
}
|
|
82
110
|
return (_jsx(FileUploadInput, { config: config, ...await getInputRendererParameters(this, params) }));
|
|
83
111
|
})();
|
|
84
112
|
}
|
|
@@ -103,6 +131,18 @@ export class ImageField extends ListableFormField {
|
|
|
103
131
|
return (async () => {
|
|
104
132
|
const value = await props.item;
|
|
105
133
|
if (value[this.name]) {
|
|
134
|
+
// 값이 단순 외부 URL 문자열로 들어온 케이스 — 자체 asset 서버를 거치지 않고
|
|
135
|
+
// 그대로 썸네일/확대 미리보기 렌더링.
|
|
136
|
+
const externalUrl = pickExternalUrl(value[this.name]);
|
|
137
|
+
if (externalUrl) {
|
|
138
|
+
return {
|
|
139
|
+
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: externalUrl, onError: (event) => {
|
|
140
|
+
event.currentTarget.src = getEndpoint('noImageFallback');
|
|
141
|
+
}, alt: "primary image" }), _jsx("div", { className: "rcm-image-field-preview-wrap", children: _jsx("img", { className: "rcm-image-field-preview", src: externalUrl, onError: (event) => {
|
|
142
|
+
event.currentTarget.src = getEndpoint('noImageFallback');
|
|
143
|
+
}, alt: "enlarged image" }) })] }) })),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
106
146
|
const file = value[this.name];
|
|
107
147
|
if (!isEmpty(file.existFiles)) {
|
|
108
148
|
const imgUrl = getAccessableAssetUrl(file.existFiles[0].url);
|
package/dist/listgrid/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { UrlStateProvider, useQueryStates, createParser, parseAsString } from '.
|
|
|
25
25
|
export type { UrlStateServices, UrlParser, UrlStateSetOptions, QueryStatesSetter, UrlStateProviderProps, } from './urlState';
|
|
26
26
|
export { configureApiClient, callExternalHttpRequest, getExternalApiData, getExternalApiDataWithError, ResponseData, createResponseData, } from './api';
|
|
27
27
|
export type { ApiClient, ApiRequestOptions, ApiMethod, IEntityError, IEntityErrorBody, } from './api';
|
|
28
|
-
export { RegexAlias, RegexEmailAddress, RegexLowerEnglishNumber, RegexPasswordNormal, RegexPhoneNumber, RegexTelephoneNumber, RegexUrlBody, fDate, fDateTime, fToNow, getFormattedTime, formatPrice, isEmpty, isEquals, isEqualsIgnoreCase, isEqualCollection, isPositive, normalizeUrl, removeTrailingSeparator, parse, getLocalStorageItem, setLocalStorageItem, getSessionStorageObject, setSessionStorageItem, ASSET_SERVER_URL, configureAssetServerUrl, getAccessableAssetUrl, removeAssetServerPrefix, getDefinedDates, } from './misc';
|
|
28
|
+
export { RegexAlias, RegexEmailAddress, RegexLowerEnglishNumber, RegexPasswordNormal, RegexPhoneNumber, RegexTelephoneNumber, RegexUrlBody, fDate, fDateTime, fToNow, getFormattedTime, formatPrice, isEmpty, isEquals, isEqualsIgnoreCase, isEqualCollection, isPositive, normalizeUrl, removeTrailingSeparator, parse, getLocalStorageItem, setLocalStorageItem, getSessionStorageObject, setSessionStorageItem, ASSET_SERVER_URL, configureAssetServerUrl, getAccessableAssetUrl, isExternalUrl, removeAssetServerPrefix, getDefinedDates, } from './misc';
|
|
29
29
|
export type { DefinedDateType } from './misc';
|
|
30
30
|
export { ViewListGrid } from './components/list/ViewListGrid';
|
|
31
31
|
export { ViewEntityForm } from './components/form/ViewEntityForm';
|
package/dist/listgrid/index.js
CHANGED
|
@@ -33,7 +33,7 @@ export { UrlStateProvider, useQueryStates, createParser, parseAsString } from '.
|
|
|
33
33
|
// REST conventions. See src/listgrid/api/ApiClient.ts.
|
|
34
34
|
export { configureApiClient, callExternalHttpRequest, getExternalApiData, getExternalApiDataWithError, ResponseData, createResponseData, } from './api';
|
|
35
35
|
// Misc helpers & constants inherited from the original kit root barrel.
|
|
36
|
-
export { RegexAlias, RegexEmailAddress, RegexLowerEnglishNumber, RegexPasswordNormal, RegexPhoneNumber, RegexTelephoneNumber, RegexUrlBody, fDate, fDateTime, fToNow, getFormattedTime, formatPrice, isEmpty, isEquals, isEqualsIgnoreCase, isEqualCollection, isPositive, normalizeUrl, removeTrailingSeparator, parse, getLocalStorageItem, setLocalStorageItem, getSessionStorageObject, setSessionStorageItem, ASSET_SERVER_URL, configureAssetServerUrl, getAccessableAssetUrl, removeAssetServerPrefix, getDefinedDates, } from './misc';
|
|
36
|
+
export { RegexAlias, RegexEmailAddress, RegexLowerEnglishNumber, RegexPasswordNormal, RegexPhoneNumber, RegexTelephoneNumber, RegexUrlBody, fDate, fDateTime, fToNow, getFormattedTime, formatPrice, isEmpty, isEquals, isEqualsIgnoreCase, isEqualCollection, isPositive, normalizeUrl, removeTrailingSeparator, parse, getLocalStorageItem, setLocalStorageItem, getSessionStorageObject, setSessionStorageItem, ASSET_SERVER_URL, configureAssetServerUrl, getAccessableAssetUrl, isExternalUrl, removeAssetServerPrefix, getDefinedDates, } from './misc';
|
|
37
37
|
// Core Components
|
|
38
38
|
export { ViewListGrid } from './components/list/ViewListGrid';
|
|
39
39
|
export { ViewEntityForm } from './components/form/ViewEntityForm';
|
|
@@ -53,6 +53,11 @@ export declare const ASSET_PREFIX: string;
|
|
|
53
53
|
export declare function configureAssetServerUrl(url: string): void;
|
|
54
54
|
export declare function configureAssetPrefix(prefix: string): void;
|
|
55
55
|
export declare function getAccessableAssetUrl(imgUrl: string | null | undefined): string;
|
|
56
|
+
/**
|
|
57
|
+
* 입력 URL 이 외부(`http://` / `https://`)로 시작하는 절대 URL인지 검사한다.
|
|
58
|
+
* 자체 asset 서버 prefix 를 적용해서는 안 되는 값을 분기 처리할 때 사용한다.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isExternalUrl(url: string | null | undefined): boolean;
|
|
56
61
|
export declare function removeAssetServerPrefix(url: string | null | undefined): string;
|
|
57
62
|
export declare function validatedAssetFileName(fileName: string): string;
|
|
58
63
|
export type DefinedDateType = 'TODAY' | 'WEEK' | 'MONTH' | 'MONTH3' | 'MONTH6' | 'YEAR';
|
|
@@ -413,6 +413,21 @@ function effectiveAssetPrefix() {
|
|
|
413
413
|
export function getAccessableAssetUrl(imgUrl) {
|
|
414
414
|
if (!imgUrl)
|
|
415
415
|
return '';
|
|
416
|
+
// 외부 절대 URL(`http(s)://`) 이고 자체 asset 서버 host 가 아니라면 그대로 통과.
|
|
417
|
+
// `removeAssetServerPrefix` 가 절대 URL 의 스킴 콜론을 URL-encode 해버려
|
|
418
|
+
// `https%3A//...` 와 같이 망가뜨리던 이전 동작을 바로잡는다.
|
|
419
|
+
if (isExternalUrl(imgUrl)) {
|
|
420
|
+
const trimmed = imgUrl.trim();
|
|
421
|
+
const server = effectiveAssetServerUrl();
|
|
422
|
+
if (server && trimmed.startsWith(server)) {
|
|
423
|
+
// 우리 asset 서버 host 라면 prefix 를 제거한 뒤 표준 경로 처리로 폴백한다.
|
|
424
|
+
let u = removeAssetServerPrefix(trimmed);
|
|
425
|
+
if (u.startsWith('/'))
|
|
426
|
+
u = u.substring(1);
|
|
427
|
+
return effectiveAssetServerUrl() + effectiveAssetPrefix() + u;
|
|
428
|
+
}
|
|
429
|
+
return trimmed;
|
|
430
|
+
}
|
|
416
431
|
let u = removeAssetServerPrefix(imgUrl);
|
|
417
432
|
if (u.startsWith('http://') || u.startsWith('https://'))
|
|
418
433
|
return u;
|
|
@@ -420,6 +435,16 @@ export function getAccessableAssetUrl(imgUrl) {
|
|
|
420
435
|
u = u.substring(1);
|
|
421
436
|
return effectiveAssetServerUrl() + effectiveAssetPrefix() + u;
|
|
422
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* 입력 URL 이 외부(`http://` / `https://`)로 시작하는 절대 URL인지 검사한다.
|
|
440
|
+
* 자체 asset 서버 prefix 를 적용해서는 안 되는 값을 분기 처리할 때 사용한다.
|
|
441
|
+
*/
|
|
442
|
+
export function isExternalUrl(url) {
|
|
443
|
+
if (!url)
|
|
444
|
+
return false;
|
|
445
|
+
const trimmed = url.trim();
|
|
446
|
+
return trimmed.startsWith('http://') || trimmed.startsWith('https://');
|
|
447
|
+
}
|
|
423
448
|
export function removeAssetServerPrefix(url) {
|
|
424
449
|
if (!url)
|
|
425
450
|
return '';
|
|
@@ -1429,7 +1429,10 @@
|
|
|
1429
1429
|
.rcm-table td {
|
|
1430
1430
|
padding: var(--rcm-space-sm) var(--rcm-space-md);
|
|
1431
1431
|
border-bottom: var(--rcm-border-width) solid var(--rcm-color-border);
|
|
1432
|
-
text-align
|
|
1432
|
+
/* text-align 은 ViewColumn / HeaderField 가 .text-left / .text-center / .text-right
|
|
1433
|
+
utility 로 셀 단위로 지정한다. 여기에 default `text-align: left` 를 두면 utility
|
|
1434
|
+
보다 specificity 가 높아 (0,1,1) vs (0,1,0) getListFieldAlignType() 의 center
|
|
1435
|
+
정렬이 무력화된다. 브라우저 기본 th/td 가 이미 left 라 default 가 필요 없다. */
|
|
1433
1436
|
}
|
|
1434
1437
|
|
|
1435
1438
|
.rcm-table th {
|
package/dist/styles.css
CHANGED
|
@@ -6398,7 +6398,10 @@
|
|
|
6398
6398
|
.rcm-table td {
|
|
6399
6399
|
padding: var(--rcm-space-sm) var(--rcm-space-md);
|
|
6400
6400
|
border-bottom: var(--rcm-border-width) solid var(--rcm-color-border);
|
|
6401
|
-
text-align
|
|
6401
|
+
/* text-align 은 ViewColumn / HeaderField 가 .text-left / .text-center / .text-right
|
|
6402
|
+
utility 로 셀 단위로 지정한다. 여기에 default `text-align: left` 를 두면 utility
|
|
6403
|
+
보다 specificity 가 높아 (0,1,1) vs (0,1,0) getListFieldAlignType() 의 center
|
|
6404
|
+
정렬이 무력화된다. 브라우저 기본 th/td 가 이미 left 라 default 가 필요 없다. */
|
|
6402
6405
|
}
|
|
6403
6406
|
|
|
6404
6407
|
.rcm-table th {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rchemist/listgrid",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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": [
|