@evergis/react 4.0.148 → 4.0.150

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.
Files changed (26) hide show
  1. package/dist/components/Dashboard/containers/AttachmentContainer/components/ShowMoreButton.d.ts +3 -0
  2. package/dist/components/Dashboard/containers/AttachmentContainer/styled.d.ts +6 -0
  3. package/dist/components/Dashboard/containers/AttachmentContainer/useAttachmentContainer.d.ts +3 -9
  4. package/dist/components/Dashboard/containers/StructuredDataContainer/types.d.ts +6 -0
  5. package/dist/components/Dashboard/elements/ElementTable/components/AttachmentsCell.d.ts +4 -1
  6. package/dist/components/Dashboard/elements/ElementTable/components/AttachmentsCellPopup/index.d.ts +10 -0
  7. package/dist/components/Dashboard/elements/ElementTable/components/AttachmentsCellPopup/styled.d.ts +9 -0
  8. package/dist/components/Dashboard/elements/ElementTable/components/AttachmentsCellPopup.d.ts +10 -0
  9. package/dist/components/Dashboard/elements/ElementTable/components/ColorCell.d.ts +12 -0
  10. package/dist/components/Dashboard/elements/ElementTable/constants.d.ts +17 -0
  11. package/dist/components/Dashboard/elements/ElementTable/hooks/useAttachmentsCell.d.ts +5 -20
  12. package/dist/components/Dashboard/elements/ElementTable/hooks/useAttachmentsCellEdit.d.ts +28 -0
  13. package/dist/components/Dashboard/elements/ElementTable/styled.d.ts +31 -1
  14. package/dist/components/Dashboard/elements/ElementTable/types.d.ts +44 -0
  15. package/dist/components/Dashboard/hooks/index.d.ts +1 -0
  16. package/dist/components/Dashboard/hooks/useAttachmentsView.d.ts +25 -0
  17. package/dist/components/Dashboard/types.d.ts +15 -0
  18. package/dist/components/Dashboard/utils/interpolateTranslation.d.ts +8 -0
  19. package/dist/components/Dashboard/utils/sliceShownOtherItems.d.ts +5 -0
  20. package/dist/index.js +433 -64
  21. package/dist/index.js.map +1 -1
  22. package/dist/react.esm.js +432 -66
  23. package/dist/react.esm.js.map +1 -1
  24. package/dist/utils/color.d.ts +8 -0
  25. package/package.json +2 -2
  26. package/dist/components/Dashboard/grid/components/GridResizer/styled.d.ts +0 -11
package/dist/index.js CHANGED
@@ -3849,7 +3849,7 @@ const hue2rgb = (p, q, t) => {
3849
3849
  */
3850
3850
  const toHex = (value) => {
3851
3851
  const hex = Math.round(value * 255).toString(16);
3852
- return hex.padStart(2, '0');
3852
+ return hex.padStart(2, "0");
3853
3853
  };
3854
3854
  /**
3855
3855
  * Adjusts color brightness
@@ -3859,7 +3859,7 @@ const toHex = (value) => {
3859
3859
  */
3860
3860
  const adjustColor = (color, lightnessAdjustment = 5) => {
3861
3861
  // Convert hex to RGB
3862
- const hex = color.replace('#', '');
3862
+ const hex = color.replace("#", "");
3863
3863
  const r = parseInt(hex.substring(0, 2), 16) / 255;
3864
3864
  const g = parseInt(hex.substring(2, 4), 16) / 255;
3865
3865
  const b = parseInt(hex.substring(4, 6), 16) / 255;
@@ -3901,6 +3901,17 @@ const adjustColor = (color, lightnessAdjustment = 5) => {
3901
3901
  }
3902
3902
  return `#${toHex(rNew)}${toHex(gNew)}${toHex(bNew)}`;
3903
3903
  };
3904
+ const OPAQUE_ALPHA = "ff";
3905
+ /**
3906
+ * Цвет в hex: `#rrggbb`, а при неполной непрозрачности — `#rrggbbaa`.
3907
+ *
3908
+ * Полностью непрозрачная альфа отбрасывается: в значении она ничего не уточняет, а строку
3909
+ * удлиняет — и в конфиге, и в данных удобнее короткая запись.
3910
+ */
3911
+ const colorToHex = (color) => {
3912
+ const hex = color.toString("hex");
3913
+ return hex.slice(HEX_RRGGBB_LENGTH).toLowerCase() === OPAQUE_ALPHA ? hex.slice(0, HEX_RRGGBB_LENGTH) : hex;
3914
+ };
3904
3915
 
3905
3916
  const NO_CONTENT_VALUE = "—";
3906
3917
  exports.DateFormat = void 0;
@@ -5585,12 +5596,22 @@ const ServerNotificationsProvider = ({ url, initialized, apiClient, children })
5585
5596
  }, children: children }));
5586
5597
  };
5587
5598
 
5599
+ const INTERPOLATION_PATTERN = /\{\{\s*(\w+)\s*\}\}/g;
5600
+ /**
5601
+ * Подставляет в строку перевода переменные вида `{{total}}` — так, как это делает i18next.
5602
+ *
5603
+ * Нужна запасному переводчику, когда хост не передал свой `t` (например, в Storybook): без неё
5604
+ * `defaultValue` с переменными уходит в интерфейс как есть. Переменная без значения остаётся
5605
+ * в тексте, чтобы пропуск был виден.
5606
+ */
5607
+ const interpolateTranslation = (text, values = {}) => text.replace(INTERPOLATION_PATTERN, (match, name) => values[name] === undefined || values[name] === null ? match : String(values[name]));
5608
+
5588
5609
  const useGlobalContext = () => {
5589
5610
  const { t, language, themeName, api, ewktGeometry, ewktExtent, zoomLevel, projectName, projectAlias, notification } = React.useContext(GlobalContext) || {};
5590
5611
  const translate = React.useCallback((value, options) => {
5591
5612
  if (t)
5592
5613
  return t(value, options);
5593
- return options?.defaultValue ?? value;
5614
+ return interpolateTranslation(options?.defaultValue ?? value, options);
5594
5615
  }, [t]);
5595
5616
  return React.useMemo(() => ({
5596
5617
  t: translate,
@@ -6016,6 +6037,30 @@ const useAttachmentPreviewImages = ({ items, active, }) => {
6016
6037
  }), [items, blobUrls, failedLinks]);
6017
6038
  };
6018
6039
 
6040
+ /**
6041
+ * Вид списка вложений: плитка или строки и сколько файлов из списка показано.
6042
+ *
6043
+ * Общий для контейнера вложений и колонки таблицы: список у них один и тот же, и считать
6044
+ * видимое дважды незачем. Предел приходит числом, а не опциями конфига, — у колонки опций
6045
+ * вида нет, он у неё зашит константой.
6046
+ */
6047
+ const useAttachmentsView = ({ items, limit, initialViewMode = "grid", }) => {
6048
+ const [viewMode, setViewMode] = React.useState(initialViewMode);
6049
+ const [showMore, setShowMore] = React.useState(false);
6050
+ const visibleItems = React.useMemo(() => (limit && !showMore ? items.slice(0, limit) : items), [items, limit, showMore]);
6051
+ const hiddenCount = items.length - visibleItems.length;
6052
+ const handleSetViewMode = React.useCallback((mode) => setViewMode(mode), []);
6053
+ return {
6054
+ visibleItems,
6055
+ hiddenCount,
6056
+ hasMore: hiddenCount > 0,
6057
+ showMore,
6058
+ setShowMore,
6059
+ viewMode,
6060
+ setViewMode: handleSetViewMode,
6061
+ };
6062
+ };
6063
+
6019
6064
  const useAutoCompleteControl = (items) => {
6020
6065
  const [value, setValue] = React.useState("");
6021
6066
  const [options, setOptions] = React.useState([]);
@@ -8556,9 +8601,23 @@ const AttachmentsViewControls = styled.div.withConfig({ displayName: "Attachment
8556
8601
  font-size: 1rem;
8557
8602
  }
8558
8603
  `;
8604
+ /**
8605
+ * Счётчик файлов у подписи — плоский чип в строку подписи.
8606
+ *
8607
+ * Шрифт ставится на текст чипа, а не на корень: текст задаёт себе шрифт шорткатом `font` из темы,
8608
+ * и размер с корня до цифры не доходит — чип выходил высоким, во весь шрифт описания.
8609
+ */
8559
8610
  const AttachmentsCountChip = styled(uilibGl.Chip).withConfig({ displayName: "AttachmentsCountChip", componentId: "sc-117ib6u" }) `
8560
- padding: 2px 6px;
8561
- font-size: 0.625rem;
8611
+ && {
8612
+ padding: 0.0625rem 0.25rem;
8613
+ border-radius: 0.75rem;
8614
+ }
8615
+
8616
+ span {
8617
+ font-size: 0.625rem;
8618
+ line-height: 0.75rem;
8619
+ font-weight: bold;
8620
+ }
8562
8621
  `;
8563
8622
  const AttachmentsContent = styled.div.withConfig({ displayName: "AttachmentsContent", componentId: "sc-1j2thff" }) `
8564
8623
  width: 100%;
@@ -8751,22 +8810,31 @@ const LinkDialogContent = styled.div.withConfig({ displayName: "LinkDialogConten
8751
8810
  padding: 1rem 0;
8752
8811
  `;
8753
8812
 
8813
+ /**
8814
+ * Заглушка недоступного изображения.
8815
+ *
8816
+ * Масштаб значка считается от размера самого превью: `font-size` контейнера приравнен к нему, а
8817
+ * бокс значка и его глиф заданы одной долей от этого размера. Раньше бокс был долей превью, а
8818
+ * глиф — фиксированным: в списке, где превью всего `1.5rem`, глиф выходил втрое крупнее своего
8819
+ * бокса и съезжал относительно квадрата.
8820
+ */
8754
8821
  const ImagePreviewError = styled.div.withConfig({ displayName: "ImagePreviewError", componentId: "sc-7cwrhm" }) `
8755
8822
  display: flex;
8756
8823
  align-items: center;
8757
8824
  justify-content: center;
8758
8825
  width: 100%;
8759
8826
  height: 100%;
8827
+ font-size: ${({ $size = GRID_TILE_SIZE }) => $size};
8760
8828
  background-color: ${({ theme }) => theme.palette.elementDark};
8761
8829
  border-radius: ${({ theme: { borderRadius: themeBorder }, borderRadius }) => borderRadius || themeBorder.smallest};
8762
8830
 
8763
8831
  ${uilibGl.Icon} {
8764
- width: 37.5%;
8765
- height: 37.5%;
8832
+ width: 0.375em;
8833
+ height: 0.375em;
8766
8834
  }
8767
8835
 
8768
8836
  ${uilibGl.Icon}:after {
8769
- font-size: 1.5rem;
8837
+ font-size: 0.375em;
8770
8838
  color: ${({ theme }) => theme.palette.textSecondary};
8771
8839
  }
8772
8840
  `;
@@ -8804,7 +8872,7 @@ const FileImagePreview = ({ link, isExternal, size, borderRadius, }) => {
8804
8872
  URL.revokeObjectURL(objectUrl);
8805
8873
  };
8806
8874
  }, [api, link, isExternal]);
8807
- return (jsxRuntime.jsxs(ImagePreviewContainer, { size: size, children: [hasError && (jsxRuntime.jsx(ImagePreviewError, { borderRadius: borderRadius, children: jsxRuntime.jsx(uilibGl.Icon, { kind: "alert" }) })), !hasError && !imageSrc && (jsxRuntime.jsx(ImagePreviewLoaderContainer, { children: jsxRuntime.jsx(uilibGl.LinearProgress, {}) })), !hasError && imageSrc && (jsxRuntime.jsx(GridImagePreview, { borderRadius: borderRadius, size: size, src: imageSrc, alt: "", onError: () => setHasError(true) }))] }));
8875
+ return (jsxRuntime.jsxs(ImagePreviewContainer, { size: size, children: [hasError && (jsxRuntime.jsx(ImagePreviewError, { borderRadius: borderRadius, "$size": size, children: jsxRuntime.jsx(uilibGl.Icon, { kind: "alert" }) })), !hasError && !imageSrc && (jsxRuntime.jsx(ImagePreviewLoaderContainer, { children: jsxRuntime.jsx(uilibGl.LinearProgress, {}) })), !hasError && imageSrc && (jsxRuntime.jsx(GridImagePreview, { borderRadius: borderRadius, size: size, src: imageSrc, alt: "", onError: () => setHasError(true) }))] }));
8808
8876
  };
8809
8877
 
8810
8878
  const AttachmentItem = ({ item, viewMode, isEdit, onPreview, onDelete, }) => {
@@ -8832,38 +8900,30 @@ const AttachmentsList = ({ items, isEdit, onPreview, onDelete, }) => {
8832
8900
  return (jsxRuntime.jsx(ListContainer, { children: items.map(item => (jsxRuntime.jsx(AttachmentItem, { item: item, viewMode: "list", isEdit: isEdit, onPreview: onPreview, onDelete: onDelete }, item.link))) }));
8833
8901
  };
8834
8902
 
8835
- const ShowMoreButton = ({ hiddenCount, onClick, }) => {
8903
+ const ShowMoreButton = ({ hiddenCount, iconKind = "arrow_down", onClick, }) => {
8836
8904
  const { t } = useGlobalContext();
8837
- return (jsxRuntime.jsxs(ShowMoreButton$1, { onClick: onClick, children: [t("showMore", { ns: "dashboard", defaultValue: "Показать ещё" }), " ", hiddenCount, jsxRuntime.jsx(uilibGl.Icon, { kind: "arrow_down" })] }));
8905
+ return (jsxRuntime.jsxs(ShowMoreButton$1, { onClick: onClick, children: [t("showMore", { ns: "dashboard", defaultValue: "Показать ещё" }), " ", hiddenCount, jsxRuntime.jsx(uilibGl.Icon, { kind: iconKind })] }));
8838
8906
  };
8839
8907
 
8908
+ /**
8909
+ * Сколько элементов показывать сразу. Заданы обе опции — выигрывает меньшая, не задана ни одна —
8910
+ * предела нет и список показывается целиком.
8911
+ */
8912
+ const getShownItemsLimit = ({ shownItems, otherItems } = {}) => shownItems && otherItems ? Math.min(shownItems, otherItems) : shownItems || otherItems;
8840
8913
  const sliceShownOtherItems = (data, options = {}, showMore) => {
8841
- const { shownItems, otherItems } = options || {};
8842
- const limit = shownItems && otherItems ? Math.min(shownItems, otherItems) : shownItems || otherItems;
8843
- return (shownItems || otherItems) && !showMore ? (data?.slice(0, limit) || []) : data;
8914
+ const limit = getShownItemsLimit(options);
8915
+ return limit && !showMore ? (data?.slice(0, limit) || []) : data;
8844
8916
  };
8845
8917
 
8846
8918
  const useAttachmentContainer = ({ type, elementConfig, valueOverride, }) => {
8847
8919
  const { items, attributeName } = useAttachmentItems({ type, elementConfig, valueOverride });
8848
8920
  const { options } = elementConfig || {};
8849
- const initialViewMode = options?.viewMode === "list" ? "list" : "grid";
8850
- const [viewMode, setViewMode] = React.useState(initialViewMode);
8851
- const [showMore, setShowMore] = React.useState(false);
8852
- const visibleItems = React.useMemo(() => sliceShownOtherItems(items, options, showMore), [items, options, showMore]);
8853
- const hiddenCount = items.length - visibleItems.length;
8854
- const hasMore = hiddenCount > 0;
8855
- const handleSetViewMode = React.useCallback((mode) => setViewMode(mode), []);
8856
- return {
8921
+ const view = useAttachmentsView({
8857
8922
  items,
8858
- visibleItems,
8859
- hiddenCount,
8860
- hasMore,
8861
- showMore,
8862
- setShowMore,
8863
- viewMode,
8864
- setViewMode: handleSetViewMode,
8865
- attributeName,
8866
- };
8923
+ limit: getShownItemsLimit(options),
8924
+ initialViewMode: options?.viewMode === "list" ? "list" : "grid",
8925
+ });
8926
+ return { ...view, items, attributeName };
8867
8927
  };
8868
8928
 
8869
8929
  const AttachmentContainer = React.memo(({ type, elementConfig, renderElement }) => {
@@ -11466,7 +11526,11 @@ const toSchemaAttribute = (params) => {
11466
11526
  width: description?.width,
11467
11527
  resizable: description?.resizable,
11468
11528
  multiline: description?.multiline,
11529
+ colorPicker: description?.colorPicker,
11469
11530
  style: description?.style,
11531
+ // Настройки загрузки файлов в колонку вложений: у источника их тоже нет.
11532
+ parentResourceId: description?.parentResourceId,
11533
+ fileExtensions: description?.fileExtensions,
11470
11534
  };
11471
11535
  };
11472
11536
  /**
@@ -13757,6 +13821,8 @@ const AddButton = ({ multiple = true, accept, onSelectFiles, onSelectFromCatalog
13757
13821
  value: AddAttachmentSource.Catalog,
13758
13822
  icon: "folder_outline",
13759
13823
  text: t("attachments.fromCatalog", { ns: "common", defaultValue: "Из каталога" }),
13824
+ // Диалог каталога выдаёт виджету приложение: не выдан — и выбирать нечем.
13825
+ disabled: !onSelectFromCatalog,
13760
13826
  },
13761
13827
  {
13762
13828
  value: AddAttachmentSource.Link,
@@ -13764,7 +13830,7 @@ const AddButton = ({ multiple = true, accept, onSelectFiles, onSelectFromCatalog
13764
13830
  text: t("attachments.fromLink", { ns: "common", defaultValue: "По ссылке" }),
13765
13831
  disabled: !onSelectFromLink,
13766
13832
  },
13767
- ], [t, onSelectFromLink]);
13833
+ ], [t, onSelectFromCatalog, onSelectFromLink]);
13768
13834
  const handleMenuSelect = React.useCallback(([{ value }]) => {
13769
13835
  if (value === AddAttachmentSource.Pc) {
13770
13836
  inputRef.current?.click();
@@ -15479,6 +15545,92 @@ const ElementSvg = React.memo(({ type, elementConfig, ...rest }) => {
15479
15545
  return (jsxRuntime.jsx(SvgImage, { url: getSvgUrl({ elementConfig, layerInfo, attributes }), width: width, height: height, fontColor: fontColor }));
15480
15546
  });
15481
15547
 
15548
+ /**
15549
+ * Вложения ячейки в поп-апе. Ширина своя, а не по ячейке: колонка таблицы бывает узкой,
15550
+ * а внутри стоит полный список вложений с плиткой и кнопкой добавления.
15551
+ *
15552
+ * Список взят у контейнера вложений карточки, а величины у поп-апа свои, с макета: имя файла
15553
+ * крупнее и ссылочного цвета, значок в строку имени, «Показать ещё» серое. Переопределены они
15554
+ * здесь, а не в самих компонентах, чтобы вложения в карточке объекта остались как были.
15555
+ */
15556
+ const AttachmentsPopupBox = styled.div.withConfig({ displayName: "AttachmentsPopupBox", componentId: "sc-1dv1wz6" }) `
15557
+ display: flex;
15558
+ flex-direction: column;
15559
+ box-sizing: border-box;
15560
+ width: 19.5rem;
15561
+ padding: 1.25rem;
15562
+
15563
+ ${AttachmentsLabel} {
15564
+ gap: 0 0.25rem;
15565
+ line-height: 0.875rem;
15566
+ }
15567
+
15568
+ ${AttachmentsViewControls} {
15569
+ gap: 0 0.5rem;
15570
+
15571
+ ${uilibGl.IconToggleButton}, ${uilibGl.Icon} {
15572
+ width: 0.875rem;
15573
+ height: 0.875rem;
15574
+ }
15575
+
15576
+ ${uilibGl.Icon}:after {
15577
+ font-size: 0.875rem;
15578
+ }
15579
+ }
15580
+
15581
+ ${ListItemMeta} {
15582
+ ${ListIcon}, ${ImagePreviewContainer}, ${GridImagePreview} {
15583
+ width: 1rem;
15584
+ height: 1rem;
15585
+ background-size: contain;
15586
+ }
15587
+ }
15588
+
15589
+ ${ListItemDescription} {
15590
+ gap: 0;
15591
+ }
15592
+
15593
+ ${ListItemName} {
15594
+ font-size: 1rem;
15595
+ line-height: 1.125rem;
15596
+ color: ${({ theme }) => theme.palette.primary};
15597
+ }
15598
+
15599
+ ${ListItemDate} {
15600
+ font-size: 0.75rem;
15601
+ line-height: 0.875rem;
15602
+ }
15603
+
15604
+ ${ShowMoreButton$1} {
15605
+ gap: 0 0.25rem;
15606
+ margin-top: 0.5rem;
15607
+ line-height: 0.875rem;
15608
+ color: ${({ theme }) => theme.palette.textSecondary};
15609
+
15610
+ ${uilibGl.Icon} {
15611
+ width: 0.875rem;
15612
+ height: 0.875rem;
15613
+ }
15614
+
15615
+ ${uilibGl.Icon}:after {
15616
+ font-size: 0.875rem;
15617
+ color: ${({ theme }) => theme.palette.textSecondary};
15618
+ }
15619
+ }
15620
+ `;
15621
+
15622
+ /**
15623
+ * Полный список вложений колонки — тот же набор, что у контейнера вложений в карточке объекта:
15624
+ * шапка с подписью, счётчиком и переключателем вида, сам список, «Показать ещё» и добавление.
15625
+ *
15626
+ * Кнопка добавления и корзины у файлов появляются только на правку: на чтение поп-ап нужен
15627
+ * ровно затем, чтобы развернуть свёрнутый счётчик.
15628
+ */
15629
+ const AttachmentsCellPopup = React.memo(({ alias, state }) => {
15630
+ const { items, visibleItems, editable, viewMode, setViewMode, hasMore, hiddenCount, onShowMore, accept, onPreview, onDelete, onUpload, onSelectFromCatalog, onOpenLinkDialog, } = state;
15631
+ return (jsxRuntime.jsxs(AttachmentsPopupBox, { children: [jsxRuntime.jsx(AttachmentsHeader, { alias: alias, count: items.length, viewMode: viewMode, onChangeViewMode: setViewMode }), jsxRuntime.jsx(AttachmentsContent, { children: viewMode === "grid" ? (jsxRuntime.jsx(AttachmentsGrid, { items: visibleItems, isEdit: editable, onPreview: onPreview, onDelete: onDelete })) : (jsxRuntime.jsx(AttachmentsList, { items: visibleItems, isEdit: editable, onPreview: onPreview, onDelete: onDelete })) }), hasMore && jsxRuntime.jsx(ShowMoreButton, { hiddenCount: hiddenCount, iconKind: "expand", onClick: onShowMore }), editable && (jsxRuntime.jsx(AddButton, { accept: accept, onSelectFiles: onUpload, onSelectFromCatalog: onSelectFromCatalog, onSelectFromLink: onOpenLinkDialog }))] }));
15632
+ });
15633
+
15482
15634
  /**
15483
15635
  * Насколько тело таблицы заехало под липкую шапку. Пишется на таблицу при прокрутке, читается
15484
15636
  * стилем тела: ровно на эту величину тело и отсекается.
@@ -15499,6 +15651,23 @@ const TIME_FORMAT = /hh:mm/;
15499
15651
  const PORTAL_ROOT_SELECTOR = "#portal-root";
15500
15652
  /** Ниже этой ширины колонку не ужать мышью: уже неё в ячейке не остаётся места под значение. */
15501
15653
  const MIN_COLUMN_WIDTH = 48;
15654
+ /**
15655
+ * Цвет, с которого палитра открывается у пустой ячейки. Своего значения там ещё нет, а `Color`
15656
+ * без валидной строки не собрать.
15657
+ */
15658
+ const DEFAULT_CELL_COLOR = "#000000";
15659
+ /**
15660
+ * Сколько вложений ячейка показывает сама. Больше — все схлопываются в одну строку со значком
15661
+ * ссылки и счётчиком: колонка в таблице узкая, и списком там помещается ровно один файл.
15662
+ */
15663
+ const ATTACHMENTS_INLINE_LIMIT = 1;
15664
+ /**
15665
+ * Сколько файлов видно в поп-апе вложений сразу, остальные прячутся за «Показать ещё N».
15666
+ * Величина с макета и опцией не выносится: поп-ап у колонки один на все таблицы.
15667
+ */
15668
+ const ATTACHMENTS_SHOWN_ITEMS = 3;
15669
+ /** Вид списка, с которого поп-ап вложений открывается. */
15670
+ const ATTACHMENTS_VIEW_MODE = "list";
15502
15671
 
15503
15672
  /**
15504
15673
  * Собственный бокс таблицы. Без размеров в `options` представления узел ничего не меняет:
@@ -15725,36 +15894,177 @@ const CellField = styled.div.withConfig({ displayName: "CellField", componentId:
15725
15894
  */
15726
15895
  const AttachmentsCellBox = styled.div.withConfig({ displayName: "AttachmentsCellBox", componentId: "sc-1ayq7sq" }) `
15727
15896
  display: flex;
15728
- flex-direction: column;
15729
- align-items: flex-start;
15730
- gap: 0.25rem;
15897
+ align-items: center;
15898
+ gap: 0.375rem;
15731
15899
  padding: 0.375rem 0.25rem;
15900
+ min-width: 0;
15901
+ line-height: 1.25rem;
15902
+ /* Пустая ячейка на чтении не делает по клику ничего — и на указатель ей меняться незачем. */
15903
+ cursor: ${({ $interactive }) => ($interactive ? "pointer" : "default")};
15904
+ `;
15905
+ /**
15906
+ * Значок файла в ячейке. Тот же, что в списке вложений, только с макетную величину строки
15907
+ * таблицы: в контейнере строка свободнее и значок там крупнее.
15908
+ */
15909
+ const AttachmentsCellIcon = styled(ListIcon).withConfig({ displayName: "AttachmentsCellIcon", componentId: "sc-wf195o" }) `
15910
+ width: 1rem;
15911
+ height: 1rem;
15912
+ background-size: contain;
15913
+ `;
15914
+ /**
15915
+ * Подпись файла или счётчика свёрнутых. Ссылочная: по ней и кликают — одиночный файл открывает
15916
+ * галерею, счётчик разворачивает список.
15917
+ */
15918
+ const AttachmentsCellLabel = styled.div.withConfig({ displayName: "AttachmentsCellLabel", componentId: "sc-11wbjsl" }) `
15919
+ color: ${({ theme }) => theme.palette.primary};
15920
+
15921
+ ${cellWrapMixin};
15922
+ `;
15923
+ /** Значок ссылки у счётчика свёрнутых файлов — в цвет подписи. */
15924
+ const AttachmentsCellLinkIcon = styled(uilibGl.Icon).withConfig({ displayName: "AttachmentsCellLinkIcon", componentId: "sc-oxcx2o" }) `
15925
+ flex-shrink: 0;
15926
+ width: 1rem;
15927
+ height: 1rem;
15928
+
15929
+ &:after {
15930
+ font-size: 1rem;
15931
+ color: ${({ theme }) => theme.palette.primary};
15932
+ }
15933
+ `;
15934
+ /**
15935
+ * Ячейка цвета: плашка или палитра слева, значение справа.
15936
+ *
15937
+ * Отступы те же, что у текстовой ячейки: в одном ряду с обычными колонками значения обязаны
15938
+ * стоять на одной линии.
15939
+ */
15940
+ const ColorCellBox = styled.div.withConfig({ displayName: "ColorCellBox", componentId: "sc-1x8c5gd" }) `
15941
+ display: flex;
15942
+ align-items: center;
15943
+ gap: 0.375rem;
15944
+ padding: 0 0.25rem;
15945
+
15946
+ ${CellText} {
15947
+ padding-left: 0;
15948
+ padding-right: 0;
15949
+ }
15950
+ `;
15951
+ /**
15952
+ * Плашка цвета. Повторяет маркер легенды графика: цвет всюду обозначается одинаково, где бы он
15953
+ * ни показывался.
15954
+ */
15955
+ const ColorSwatch = styled.div.withConfig({ displayName: "ColorSwatch", componentId: "sc-1h7m0yr" }) `
15956
+ flex-shrink: 0;
15957
+ width: 0.75rem;
15958
+ height: 0.75rem;
15959
+ background-color: ${({ $color }) => $color};
15960
+ border-radius: ${({ theme: { borderRadius } }) => borderRadius.tiny};
15732
15961
  `;
15733
- const TableEmpty = styled.div.withConfig({ displayName: "TableEmpty", componentId: "sc-dmdhrv" }) `
15962
+ const TableEmpty = styled.div.withConfig({ displayName: "TableEmpty", componentId: "sc-10hkdmv" }) `
15734
15963
  padding: 0.75rem 0.25rem;
15735
15964
  color: ${({ theme }) => theme.palette.textSecondary};
15736
15965
  `;
15737
15966
 
15738
15967
  /**
15739
- * Ячейка с вложениями: разбор значения, просмотр, скачивание и правка списка.
15968
+ * Добавление и удаление файлов в колонке вложений.
15969
+ *
15970
+ * Все три источника — диск, каталог ресурсов и ссылка — те же, что у контейнера вложений
15971
+ * в карточке объекта: файловое api берётся из глобального контекста, диалог каталога выдаёт
15972
+ * виджету приложение, а ссылка разбирается по адресу.
15973
+ *
15974
+ * Папку-приёмник загрузки задаёт сам атрибут: у колонки нет узла-элемента с опциями, зато
15975
+ * колонок вложений в таблице может быть несколько, и складывать их файлы в одно место незачем.
15976
+ */
15977
+ const useAttachmentsCellEdit = ({ attribute, items, persist }) => {
15978
+ const { api } = useGlobalContext();
15979
+ const { selectAttachmentsFromCatalog } = useWidgetContext();
15980
+ const [isLinkDialogOpen, , setLinkDialogOpen] = useToggle(false);
15981
+ const [uploading, setUploading] = React.useState(false);
15982
+ const { parentResourceId, fileExtensions } = attribute;
15983
+ const onDelete = React.useCallback((link) => persist(items.filter(item => item.link !== link)), [items, persist]);
15984
+ const onUpload = React.useCallback(async (files) => {
15985
+ if (!api?.file?.upload || uploading) {
15986
+ return;
15987
+ }
15988
+ setUploading(true);
15989
+ try {
15990
+ const uploaded = await Promise.all(files.map(file => api.file.upload(file, true, parentResourceId || "", file.name)));
15991
+ persist([
15992
+ ...items,
15993
+ ...uploaded.map((response, index) => ({
15994
+ link: response.resourceId,
15995
+ name: response.name ?? files[index].name,
15996
+ mimeType: files[index].type,
15997
+ date: new Date().toISOString(),
15998
+ isExternal: false,
15999
+ })),
16000
+ ]);
16001
+ }
16002
+ finally {
16003
+ setUploading(false);
16004
+ }
16005
+ }, [api, items, parentResourceId, persist, uploading]);
16006
+ const onAddFromCatalog = React.useCallback((resources) => persist([
16007
+ ...items,
16008
+ ...resources.map(({ resourceId, name, contentType }) => ({
16009
+ link: resourceId ?? "",
16010
+ name: name ?? "",
16011
+ mimeType: contentType ?? "",
16012
+ date: new Date().toISOString(),
16013
+ isExternal: false,
16014
+ })),
16015
+ ]), [items, persist]);
16016
+ const onSelectFromCatalog = React.useMemo(() => (selectAttachmentsFromCatalog ? () => selectAttachmentsFromCatalog(onAddFromCatalog) : undefined), [onAddFromCatalog, selectAttachmentsFromCatalog]);
16017
+ const onOpenLinkDialog = React.useCallback(() => setLinkDialogOpen(true), [setLinkDialogOpen]);
16018
+ const onCloseLinkDialog = React.useCallback(() => setLinkDialogOpen(false), [setLinkDialogOpen]);
16019
+ const onAddByLink = React.useCallback((url) => persist([
16020
+ ...items,
16021
+ {
16022
+ link: url,
16023
+ name: getFileNameFromUrl(url),
16024
+ mimeType: getMimeTypeFromUrl(url),
16025
+ date: new Date().toISOString(),
16026
+ isExternal: true,
16027
+ },
16028
+ ]), [items, persist]);
16029
+ return {
16030
+ accept: fileExtensions,
16031
+ isLinkDialogOpen,
16032
+ onDelete,
16033
+ onUpload,
16034
+ onSelectFromCatalog,
16035
+ onOpenLinkDialog,
16036
+ onCloseLinkDialog,
16037
+ onAddByLink,
16038
+ };
16039
+ };
16040
+
16041
+ /**
16042
+ * Колонка вложений: разбор значения, свёрнутый вид ячейки и полный список в поп-апе.
15740
16043
  *
15741
16044
  * Значение атрибута — строка: файлы приезжают в ней списком JSON, поэтому и обратно в черновик
15742
16045
  * кладём строку, а не массив — иначе тип ячейки менялся бы от самой правки.
15743
16046
  *
15744
- * Добавление здесь только по ссылке. Загрузка с диска требует ресурса-родителя и файлового
15745
- * api контейнера вложений, а у колонки таблицы ни того, ни другого нет.
16047
+ * Ячейка показывает один файл, а несколько схлопывает в счётчик, и весь список с добавлением
16048
+ * и удалением живёт в поп-апе тот же набор, что у контейнера вложений в карточке объекта.
15746
16049
  */
15747
- const useAttachmentsCell = ({ attribute, row, canEdit, onChange }) => {
16050
+ const useAttachmentsCell = ({ attribute, row, canEdit, onChange, }) => {
15748
16051
  const [previewIndex, setPreviewIndex] = React.useState(null);
15749
- const [isLinkDialogOpen, setLinkDialogOpen] = React.useState(false);
16052
+ const [isPopupOpen, , setPopupOpen] = useToggle(false);
15750
16053
  const { attributeName, isEditable } = attribute;
15751
16054
  const value = row.properties[attributeName];
15752
16055
  const items = React.useMemo(() => parseAttachments(value), [value]);
16056
+ const view = useAttachmentsView({
16057
+ items,
16058
+ limit: ATTACHMENTS_SHOWN_ITEMS,
16059
+ initialViewMode: ATTACHMENTS_VIEW_MODE,
16060
+ });
16061
+ const { setShowMore } = view;
15753
16062
  const previewImages = useAttachmentPreviewImages({ items, active: previewIndex !== null });
15754
16063
  const downloadByIndex = useAttachmentDownload(items);
15755
16064
  // Те же два условия, что и у обычной ячейки: режим таблицы и разрешение самого атрибута.
15756
16065
  const editable = canEdit && isEditable;
15757
16066
  const persist = React.useCallback((next) => onChange(row.key, attributeName, JSON.stringify(next)), [attributeName, onChange, row.key]);
16067
+ const edit = useAttachmentsCellEdit({ attribute, items, persist });
15758
16068
  const onPreview = React.useCallback((link) => {
15759
16069
  const index = items.findIndex(item => item.link === link);
15760
16070
  if (index >= 0) {
@@ -15763,48 +16073,99 @@ const useAttachmentsCell = ({ attribute, row, canEdit, onChange }) => {
15763
16073
  }, [items]);
15764
16074
  const onClosePreview = React.useCallback(() => setPreviewIndex(null), []);
15765
16075
  const onDownload = React.useCallback((_image, index) => downloadByIndex(index), [downloadByIndex]);
15766
- const onDelete = React.useCallback((link) => persist(items.filter(item => item.link !== link)), [items, persist]);
15767
- const onOpenLinkDialog = React.useCallback(() => setLinkDialogOpen(true), []);
15768
- const onCloseLinkDialog = React.useCallback(() => setLinkDialogOpen(false), []);
15769
- const onAddByLink = React.useCallback((url) => persist([
15770
- ...items,
15771
- {
15772
- link: url,
15773
- name: getFileNameFromUrl(url),
15774
- mimeType: getMimeTypeFromUrl(url),
15775
- date: new Date().toISOString(),
15776
- isExternal: true,
15777
- },
15778
- ]), [items, persist]);
16076
+ const onShowMore = React.useCallback(() => setShowMore(true), [setShowMore]);
16077
+ const onClosePopup = React.useCallback(() => setPopupOpen(false), [setPopupOpen]);
16078
+ /**
16079
+ * На правку поп-ап открывается всегда, даже у пустой ячейки: другого места, откуда добавить
16080
+ * первый файл, у колонки нет. На чтение единственный файл ведёт прямо в галерею, а свёрнутый
16081
+ * счётчик — в тот же поп-ап, только без кнопок.
16082
+ */
16083
+ const onCellClick = React.useCallback(() => {
16084
+ if (!editable && items.length === ATTACHMENTS_INLINE_LIMIT) {
16085
+ onPreview(items[0].link);
16086
+ return;
16087
+ }
16088
+ if (editable || items.length) {
16089
+ setPopupOpen(true);
16090
+ }
16091
+ }, [editable, items, onPreview, setPopupOpen]);
15779
16092
  return {
16093
+ ...view,
16094
+ ...edit,
15780
16095
  items,
15781
16096
  editable,
16097
+ onShowMore,
16098
+ isPopupOpen,
16099
+ onClosePopup,
16100
+ onCellClick,
15782
16101
  previewIndex,
15783
16102
  previewImages,
15784
- isLinkDialogOpen,
15785
16103
  onPreview,
15786
16104
  onClosePreview,
15787
16105
  onDownload,
15788
- onDelete,
15789
- onOpenLinkDialog,
15790
- onCloseLinkDialog,
15791
- onAddByLink,
15792
16106
  };
15793
16107
  };
15794
16108
 
15795
16109
  /**
15796
- * Колонка со вложениями: список файлов прямо в ячейке.
16110
+ * Колонка со вложениями.
15797
16111
  *
15798
16112
  * Тип у атрибута строковый, вложениями его делает `subType: "Attachments"` в схеме контейнера —
15799
16113
  * поэтому ветка стоит раньше разбора типа и обычных редакторов ячейки.
16114
+ *
16115
+ * Сама ячейка показывает один файл, а несколько схлопывает в строку со значком ссылки
16116
+ * и счётчиком: в ширину колонки список файлов не помещается. Весь список живёт в поп-апе.
15800
16117
  */
15801
16118
  const AttachmentsCell = React.memo(({ attribute, row, canEdit, onChange }) => {
15802
16119
  const { t } = useGlobalContext();
15803
- const { items, editable, previewIndex, previewImages, isLinkDialogOpen, onPreview, onClosePreview, onDownload, onDelete, onOpenLinkDialog, onCloseLinkDialog, onAddByLink, } = useAttachmentsCell({ attribute, row, canEdit, onChange });
15804
- return (jsxRuntime.jsxs(AttachmentsCellBox, { children: [!items.length && !editable && jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }), jsxRuntime.jsx(AttachmentsList, { items: items, isEdit: editable, onPreview: onPreview, onDelete: onDelete }), editable && (jsxRuntime.jsx(uilibGl.IconButton, { kind: "link", tabIndex: 0, title: t("attachments.fromLink", { ns: "common", defaultValue: "По ссылке" }), onClick: onOpenLinkDialog })), jsxRuntime.jsx(AttachmentLinkDialog, { isOpen: isLinkDialogOpen, onClose: onCloseLinkDialog, onSubmit: onAddByLink }), previewIndex !== null && (jsxRuntime.jsx(uilibGl.Preview, { images: previewImages, initialIndex: previewIndex, isOpen: true, onClose: onClosePreview, onDownload: onDownload, errorTitleText: t("attachments.resourceUnavailable", {
16120
+ const state = useAttachmentsCell({ attribute, row, canEdit, onChange });
16121
+ const { items, editable, isPopupOpen, onClosePopup, onCellClick } = state;
16122
+ const [single] = items;
16123
+ const isCollapsed = items.length > ATTACHMENTS_INLINE_LIMIT;
16124
+ const fileType = React.useMemo(() => (single ? getFileType(single.mimeType, single.name) : undefined), [single]);
16125
+ const isImage = React.useMemo(() => !!fileType && IMAGE_FILE_TYPES.includes(fileType), [fileType]);
16126
+ // Удаление единственного файла — действие самой ячейки, поп-ап ради него открывать незачем.
16127
+ const handleDelete = React.useCallback((event) => {
16128
+ event.stopPropagation();
16129
+ state.onDelete(single.link);
16130
+ }, [single, state]);
16131
+ const renderCell = () => {
16132
+ if (isCollapsed) {
16133
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(AttachmentsCellLinkIcon, { kind: "link" }), jsxRuntime.jsx(AttachmentsCellLabel, { children: t("attachments.objectsCount", {
16134
+ ns: "common",
16135
+ total: items.length,
16136
+ defaultValue: "Объектов: {{total}}",
16137
+ }) })] }));
16138
+ }
16139
+ if (!single) {
16140
+ return jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" });
16141
+ }
16142
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [isImage ? (jsxRuntime.jsx(FileImagePreview, { size: "1rem", isExternal: single.isExternal, link: single.link })) : (jsxRuntime.jsx(AttachmentsCellIcon, { fileType: fileType })), jsxRuntime.jsx(AttachmentsCellLabel, { title: single.name, children: single.name }), editable && jsxRuntime.jsx(uilibGl.IconButton, { kind: "delete", tabIndex: 0, onClick: handleDelete })] }));
16143
+ };
16144
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(uilibGl.Popup, { zIndex: DASHBOARD_OVERLAY_Z_INDEX, isVisible: isPopupOpen, onRequestClose: onClosePopup, placement: "bottom-start", anchor: ref => (jsxRuntime.jsx(AttachmentsCellBox, { ref: ref, "$interactive": editable || !!items.length, onClick: onCellClick, children: renderCell() })), children: jsxRuntime.jsx(AttachmentsCellPopup, { alias: attribute.alias, state: state }) }), jsxRuntime.jsx(AttachmentLinkDialog, { isOpen: state.isLinkDialogOpen, onClose: state.onCloseLinkDialog, onSubmit: state.onAddByLink }), state.previewIndex !== null && (jsxRuntime.jsx(uilibGl.Preview, { images: state.previewImages, initialIndex: state.previewIndex, isOpen: true, onClose: state.onClosePreview, onDownload: state.onDownload, errorTitleText: t("attachments.resourceUnavailable", {
15805
16145
  ns: "common",
15806
16146
  defaultValue: "Ресурс недоступен",
15807
- }) }, previewIndex))] }));
16147
+ }) }, state.previewIndex))] }));
16148
+ });
16149
+
16150
+ /**
16151
+ * Колонка цвета (`colorPicker` у атрибута).
16152
+ *
16153
+ * Значение хранится строкой в любом из привычных видов — `hex`, `rgb`, `rgba`, — поэтому рядом со
16154
+ * значением показывается плашка этого цвета, а на правку встаёт палитра.
16155
+ *
16156
+ * Неразбираемая строка остаётся просто текстом, без плашки: так видно, что в данных мусор, а не
16157
+ * пустое значение.
16158
+ */
16159
+ const ColorCell = React.memo(({ attribute, row, canEdit, onChange }) => {
16160
+ const { attributeName, isEditable } = attribute;
16161
+ const value = row.properties[attributeName];
16162
+ const text = React.useMemo(() => (lodash.isNil(value) ? "" : String(value)), [value]);
16163
+ const color = React.useMemo(() => (text ? new color$1.Color(text) : null), [text]);
16164
+ const isValidColor = !!color?.isValid;
16165
+ // Те же два условия, что и у обычной ячейки: режим таблицы и разрешение самого атрибута.
16166
+ const editable = canEdit && isEditable;
16167
+ const handleChange = React.useCallback((next) => onChange(row.key, attributeName, colorToHex(next)), [attributeName, onChange, row.key]);
16168
+ return (jsxRuntime.jsxs(ColorCellBox, { children: [editable ? (jsxRuntime.jsx(uilibGl.ColorPicker, { zIndex: DASHBOARD_OVERLAY_Z_INDEX, withOpacity: true, value: isValidColor ? color : new color$1.Color(DEFAULT_CELL_COLOR), onChange: handleChange })) : (isValidColor && jsxRuntime.jsx(ColorSwatch, { "$color": text })), jsxRuntime.jsx(CellText, { title: text, children: text || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) })] }));
15808
16169
  });
15809
16170
 
15810
16171
  /**
@@ -15905,7 +16266,7 @@ const useCellEditing = () => {
15905
16266
  const TableCell = React.memo(({ attribute, row, canEdit, onChange }) => {
15906
16267
  const { t, language } = useGlobalContext();
15907
16268
  const { editing, buttonProps, editorProps } = useCellEditing();
15908
- const { attributeName, type, subType, isEditable, stringFormat, multiline } = attribute;
16269
+ const { attributeName, type, subType, isEditable, stringFormat, multiline, colorPicker } = attribute;
15909
16270
  const value = row.properties[attributeName];
15910
16271
  const handleChange = React.useCallback((next) => {
15911
16272
  // Пустые правки отбрасываем: иначе строка становилась бы «изменённой» от простого
@@ -15929,6 +16290,11 @@ const TableCell = React.memo(({ attribute, row, canEdit, onChange }) => {
15929
16290
  if (subType === api.StringSubType.Attachments) {
15930
16291
  return jsxRuntime.jsx(AttachmentsCell, { attribute: attribute, row: row, canEdit: canEdit, onChange: onChange });
15931
16292
  }
16293
+ // Цвет — тоже строка, и по типу от обычной не отличается: что она значит, говорит только
16294
+ // `colorPicker` в схеме.
16295
+ if (colorPicker) {
16296
+ return jsxRuntime.jsx(ColorCell, { attribute: attribute, row: row, canEdit: canEdit, onChange: onChange });
16297
+ }
15932
16298
  // Логическое значение и на чтение показываем галкой, а не словами «true»/«false» —
15933
16299
  // так же, как остальной дашборд рисует булевы атрибуты.
15934
16300
  if (type === api.AttributeType.Boolean) {
@@ -19816,6 +20182,7 @@ exports.buildTrackTemplate = buildTrackTemplate;
19816
20182
  exports.checkEqualOrIncludes = checkEqualOrIncludes;
19817
20183
  exports.checkIsLoading = checkIsLoading;
19818
20184
  exports.collectConfigIds = collectConfigIds;
20185
+ exports.colorToHex = colorToHex;
19819
20186
  exports.containsNodeId = containsNodeId;
19820
20187
  exports.createConfigLayer = createConfigLayer;
19821
20188
  exports.createConfigPage = createConfigPage;
@@ -19890,6 +20257,7 @@ exports.getRenderElement = getRenderElement;
19890
20257
  exports.getResourceUrl = getResourceUrl;
19891
20258
  exports.getRootElementId = getRootElementId;
19892
20259
  exports.getSelectedFilterValue = getSelectedFilterValue;
20260
+ exports.getShownItemsLimit = getShownItemsLimit;
19893
20261
  exports.getSlideshowImages = getSlideshowImages;
19894
20262
  exports.getStyleAttributes = getStyleAttributes;
19895
20263
  exports.getSvgUrl = getSvgUrl;
@@ -19957,6 +20325,7 @@ exports.useAppHeight = useAppHeight;
19957
20325
  exports.useAttachmentDownload = useAttachmentDownload;
19958
20326
  exports.useAttachmentItems = useAttachmentItems;
19959
20327
  exports.useAttachmentPreviewImages = useAttachmentPreviewImages;
20328
+ exports.useAttachmentsView = useAttachmentsView;
19960
20329
  exports.useAutoCompleteControl = useAutoCompleteControl;
19961
20330
  exports.useBeforeSave = useBeforeSave;
19962
20331
  exports.useBgImageHost = useBgImageHost;