@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.
@@ -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);
@@ -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';
@@ -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: left;
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: left;
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.2",
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": [