@evergis/react 4.0.133 → 4.0.134

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.
@@ -1 +1,10 @@
1
+ /**
2
+ * `src` для картинки: свой файл тянется fetch'ем с токеном и отдаётся blob-адресом, чужой
3
+ * возвращается как есть.
4
+ *
5
+ * Внешний адрес намеренно не фетчится: тег `img` грузит кросс-доменную картинку без всякого
6
+ * CORS, а `fetch` — только если чужой сервер отдал `Access-Control-Allow-Origin`. Заодно
7
+ * картинка остаётся в HTTP-кеше браузера, которого blob-адрес лишён. Ровно так же поступают
8
+ * вложения: `useAttachmentPreviewImages` отдаёт внешней ссылке `src: item.link`.
9
+ */
1
10
  export declare const useFetchImageWithAuth: (url: string | null) => string | null;
@@ -1 +1,11 @@
1
+ /**
2
+ * Приводит значение конфига к адресу картинки, иконки или ссылки.
3
+ *
4
+ * Развилка одна: готовый адрес отдаётся как есть, всё остальное считается идентификатором
5
+ * ресурса и разворачивается в путь к файлу каталога.
6
+ *
7
+ * Проверка идёт по схеме, а не по префиксу `http`: иначе `data:`-URI, `blob:`, `mailto:` и
8
+ * `//cdn…` превращались бы в `/sp/resources/file/data:…` — путь к несуществующему файлу,
9
+ * который дальше по течению неотличим от обычного внутреннего адреса и молча даёт 404.
10
+ */
1
11
  export declare const getResourceUrl: (url?: string) => string;
@@ -60,6 +60,7 @@ export * from './getTotalFromAttributes';
60
60
  export * from './getTotalFromRelatedFeatures';
61
61
  export * from './getRootElementId';
62
62
  export * from './hasContainerBgImage';
63
+ export * from './isCrossOriginUrl';
63
64
  export * from './isEmptyElementValue';
64
65
  export * from './isEmptyValue';
65
66
  export * from './isHiddenEmptyValue';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Ведёт ли адрес на ЧУЖОЙ origin — сервер, который ничего не знает ни про наш токен, ни про
3
+ * наши заголовки.
4
+ *
5
+ * Относительный адрес (`/sp/resources/file/<id>`) всегда свой. Абсолютный сравнивается с
6
+ * `location.origin`, поэтому `https://<наш хост>/sp/...` тоже считается своим: его файлы
7
+ * закрыты авторизацией ровно так же, как у относительного пути.
8
+ *
9
+ * Проверка НЕ сводится к `^https?://`, как `isExternal` у вложений: там достаточно отличить
10
+ * ссылку от файла каталога, а здесь решается, отправлять ли на адрес `Authorization`.
11
+ *
12
+ * Разобрать адрес не удалось — считаем его своим: пусть с ним разбирается обычный путь
13
+ * загрузки, а не эта проверка.
14
+ */
15
+ export declare const isCrossOriginUrl: (url?: string | null) => boolean;
package/dist/index.js CHANGED
@@ -7226,6 +7226,31 @@ const useEditGroupAttributes = ({ elementConfig, type }) => {
7226
7226
  return { filteredAttributes, filteredControls };
7227
7227
  };
7228
7228
 
7229
+ /**
7230
+ * Ведёт ли адрес на ЧУЖОЙ origin — сервер, который ничего не знает ни про наш токен, ни про
7231
+ * наши заголовки.
7232
+ *
7233
+ * Относительный адрес (`/sp/resources/file/<id>`) всегда свой. Абсолютный сравнивается с
7234
+ * `location.origin`, поэтому `https://<наш хост>/sp/...` тоже считается своим: его файлы
7235
+ * закрыты авторизацией ровно так же, как у относительного пути.
7236
+ *
7237
+ * Проверка НЕ сводится к `^https?://`, как `isExternal` у вложений: там достаточно отличить
7238
+ * ссылку от файла каталога, а здесь решается, отправлять ли на адрес `Authorization`.
7239
+ *
7240
+ * Разобрать адрес не удалось — считаем его своим: пусть с ним разбирается обычный путь
7241
+ * загрузки, а не эта проверка.
7242
+ */
7243
+ const isCrossOriginUrl = (url) => {
7244
+ if (!url)
7245
+ return false;
7246
+ try {
7247
+ return new URL(url, window.location.href).origin !== window.location.origin;
7248
+ }
7249
+ catch {
7250
+ return false;
7251
+ }
7252
+ };
7253
+
7229
7254
  const useFetchWithAuth = (url, transform, cleanup) => {
7230
7255
  const [data, setData] = React.useState(null);
7231
7256
  const loadingRef = React.useRef(false);
@@ -7237,7 +7262,12 @@ const useFetchWithAuth = (url, transform, cleanup) => {
7237
7262
  if (!url || loadingRef.current)
7238
7263
  return;
7239
7264
  loadingRef.current = true;
7240
- const token = window.localStorage.getItem(api.STORAGE_TOKEN_KEY);
7265
+ /*
7266
+ Токен уходит только на свой сервер. Чужому он не нужен и опасен — а вдобавок кастомный
7267
+ заголовок переводит кросс-доменный запрос в preflight-режим, который сторонние хранилища
7268
+ картинок не обслуживают: ответ на OPTIONS без Access-Control-Allow-Headers роняет весь fetch.
7269
+ */
7270
+ const token = isCrossOriginUrl(url) ? null : window.localStorage.getItem(api.STORAGE_TOKEN_KEY);
7241
7271
  fetch(url, {
7242
7272
  headers: token ? { Authorization: `Bearer ${token}` } : {},
7243
7273
  })
@@ -7266,7 +7296,20 @@ const useFetchWithAuth = (url, transform, cleanup) => {
7266
7296
  };
7267
7297
 
7268
7298
  const toObjectUrl = (res) => res.blob().then(blob => URL.createObjectURL(blob));
7269
- const useFetchImageWithAuth = (url) => useFetchWithAuth(url, toObjectUrl, URL.revokeObjectURL);
7299
+ /**
7300
+ * `src` для картинки: свой файл тянется fetch'ем с токеном и отдаётся blob-адресом, чужой
7301
+ * возвращается как есть.
7302
+ *
7303
+ * Внешний адрес намеренно не фетчится: тег `img` грузит кросс-доменную картинку без всякого
7304
+ * CORS, а `fetch` — только если чужой сервер отдал `Access-Control-Allow-Origin`. Заодно
7305
+ * картинка остаётся в HTTP-кеше браузера, которого blob-адрес лишён. Ровно так же поступают
7306
+ * вложения: `useAttachmentPreviewImages` отдаёт внешней ссылке `src: item.link`.
7307
+ */
7308
+ const useFetchImageWithAuth = (url) => {
7309
+ const isExternal = React.useMemo(() => isCrossOriginUrl(url), [url]);
7310
+ const blobUrl = useFetchWithAuth(isExternal ? null : url, toObjectUrl, URL.revokeObjectURL);
7311
+ return isExternal ? url : blobUrl;
7312
+ };
7270
7313
 
7271
7314
  const HEIGHT_OFFSET = 20;
7272
7315
  const FILL_OPACITY = 0.28;
@@ -16373,21 +16416,27 @@ const BarChartFilter = ({ type, config, elementConfig, filter }) => {
16373
16416
  const DEFAULT_FROM_DATA = { minFromData: null, maxFromData: null };
16374
16417
  const RangeDateFilter = ({ type, filter }) => {
16375
16418
  const { t } = useGlobalContext();
16376
- const { changeFilters, dataSources } = useWidgetContext(type);
16419
+ const { changeFilters, dataSources, filters } = useWidgetContext(type);
16377
16420
  const { currentPage } = useWidgetPage(type);
16378
- const [value, setValue] = React.useState([undefined, undefined]);
16379
- const isInit = React.useRef(false);
16380
- const isDefaultApplied = React.useRef(false);
16381
16421
  const updateDebounce = useDebouncedCallback(FILTER_UPDATE_DEBOUNCE);
16382
16422
  const { filters: configFilters } = currentPage;
16383
16423
  const { filterName, label, minValue, maxValue, withTime } = filter.options;
16424
+ // Локальный стейт — копия значения из общего стора фильтров, поэтому при монтировании поднимаем
16425
+ // выбор оттуда. Контейнер фильтров размонтируется на время перезапроса источников (их стор
16426
+ // сбрасывается целиком, например, на смену геометрической выборки), и без этого выбранный
16427
+ // диапазон терялся бы, а следом его затирал дефолт.
16428
+ const [value, setValue] = React.useState(() => [getDate(filters[filterName]?.min)?.toISOString(), getDate(filters[filterName]?.max)?.toISOString()]);
16429
+ const isInit = React.useRef(false);
16430
+ // Ref фиксирует только первое значение выражения: если на момент монтирования выбор уже лежит
16431
+ // в сторе, дефолт считается применённым — перемонтирование не должно его затирать.
16432
+ const isDefaultApplied = React.useRef(!lodash.isNil(value[0]) || !lodash.isNil(value[1]));
16384
16433
  const configFilter = React.useMemo(() => getConfigFilter(filterName, configFilters), [configFilters, filterName]);
16385
16434
  const dataSource = React.useMemo(() => getDataSource(configFilter?.relatedDataSource, dataSources), [configFilter?.relatedDataSource, dataSources]);
16386
16435
  const { minFromData, maxFromData } = React.useMemo(() => dataSource?.features?.reduce((prev, { properties }) => {
16387
16436
  const date = getDate(properties[configFilter?.attributeValue]);
16388
16437
  return {
16389
16438
  minFromData: !prev.minFromData ? date : date < prev.minFromData ? date : prev.minFromData,
16390
- maxFromData: !prev.maxFromData ? date : date > prev.maxFromData ? date : prev.maxFromData
16439
+ maxFromData: !prev.maxFromData ? date : date > prev.maxFromData ? date : prev.maxFromData,
16391
16440
  };
16392
16441
  }, DEFAULT_FROM_DATA) || DEFAULT_FROM_DATA, [configFilter?.attributeValue, dataSource?.features]);
16393
16442
  const minDate = React.useMemo(() => minValue || minFromData?.toISOString(), [minFromData, minValue]);
@@ -16408,8 +16457,8 @@ const RangeDateFilter = ({ type, filter }) => {
16408
16457
  [filterName]: {
16409
16458
  value: [from, to],
16410
16459
  min: from,
16411
- max: to
16412
- }
16460
+ max: to,
16461
+ },
16413
16462
  }, configFilter?.resetFilters);
16414
16463
  }, [changeFilters, configFilter?.resetFilters, defaultRange, filterName]);
16415
16464
  const onChange = React.useCallback((newValue) => {
@@ -16436,8 +16485,8 @@ const RangeDateFilter = ({ type, filter }) => {
16436
16485
  [filterName]: {
16437
16486
  value,
16438
16487
  min: value[0],
16439
- max: value[1]
16440
- }
16488
+ max: value[1],
16489
+ },
16441
16490
  }, configFilter?.resetFilters);
16442
16491
  });
16443
16492
  }, [value]); // eslint-disable-line
@@ -17186,8 +17235,26 @@ const getFormattedAttributes = (t, data, attributes, config) => {
17186
17235
  : attributes;
17187
17236
  };
17188
17237
 
17238
+ /**
17239
+ * Адрес, который уже готов к употреблению: со схемой (`https:`, `data:`, `blob:`, `mailto:`),
17240
+ * протокол-относительный (`//cdn…`) или абсолютный путь (`/sp/…`).
17241
+ *
17242
+ * Идентификатор ресурса — GUID, схемы у него нет и с `/` он не начинается, поэтому под условие
17243
+ * не попадает и по-прежнему разворачивается в путь к файлу каталога.
17244
+ */
17245
+ const READY_URL = /^(?:[a-z][a-z\d+\-.]*:|\/)/i;
17246
+ /**
17247
+ * Приводит значение конфига к адресу картинки, иконки или ссылки.
17248
+ *
17249
+ * Развилка одна: готовый адрес отдаётся как есть, всё остальное считается идентификатором
17250
+ * ресурса и разворачивается в путь к файлу каталога.
17251
+ *
17252
+ * Проверка идёт по схеме, а не по префиксу `http`: иначе `data:`-URI, `blob:`, `mailto:` и
17253
+ * `//cdn…` превращались бы в `/sp/resources/file/data:…` — путь к несуществующему файлу,
17254
+ * который дальше по течению неотличим от обычного внутреннего адреса и молча даёт 404.
17255
+ */
17189
17256
  const getResourceUrl = (url) => {
17190
- return url ? (url.startsWith("http") ? url : `/sp/resources/file/${url}`) : "";
17257
+ return url ? (READY_URL.test(url) ? url : `/sp/resources/file/${url}`) : "";
17191
17258
  };
17192
17259
 
17193
17260
  /**
@@ -18728,6 +18795,7 @@ exports.getTrackSizes = getTrackSizes;
18728
18795
  exports.getWrapperSizeStyle = getWrapperSizeStyle;
18729
18796
  exports.hasContainerBgImage = hasContainerBgImage;
18730
18797
  exports.hexToRgba = hexToRgba;
18798
+ exports.isCrossOriginUrl = isCrossOriginUrl;
18731
18799
  exports.isEmptyElementValue = isEmptyElementValue;
18732
18800
  exports.isEmptyValue = isEmptyValue;
18733
18801
  exports.isFeaturesFilterValue = isFeaturesFilterValue;