Правки для лучшей интеграции
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
3. Готово — но с оговоркой: приложение доступно **не** по фиксированному `index.html`.
|
3. Готово — но с оговоркой: приложение доступно **не** по фиксированному `index.html`.
|
||||||
|
|
||||||
**Имя `index.html` в `dist/` — с хешем содержимого**, например `index-2fddeca6.html`, а не просто `index.html` (см. `vite.config.ts`, плагин `hashIndexHtml` — хеширует и сам HTML-файл, не только JS/CSS, которые Vite и так хеширует по умолчанию). Хеш меняется при каждой значимой пересборке и не меняется, если содержимое не менялось. Значит:
|
**Имя `index.html` в `dist/` — с хешем содержимого**, например `index-2fddeca6.html`, а не просто `index.html` (см. `vite.config.ts`, плагин `hashIndexHtml` — хеширует и сам HTML-файл, не только JS/CSS, которые Vite и так хеширует по умолчанию). Хеш меняется при каждой значимой пересборке и не меняется, если содержимое не менялось. Значит:
|
||||||
- **`src` у iframe нельзя хардкодить строкой** — нужно на своей стороне подставлять актуальное имя файла. Готовый способ: рядом с билдом сборка кладёт `app.json` со **стабильным** именем и актуальным именем точки входа внутри — `{ "index": "index-2fddeca6.html" }`. Прочитать его можно и с бэкенда, и прямо из браузера (`fetch('app.json', { cache: 'no-store' })`). Альтернативы — взять единственный `index-*.html` в каталоге или отдать имя отдельным методом; выбор за бэкендом.
|
- **`src` у iframe нельзя хардкодить строкой** — нужно на своей стороне подставлять актуальное имя файла. Готовый способ: рядом с билдом сборка кладёт `app-manifest.js` со **стабильным** именем и актуальным именем точки входа внутри — `{ "index": "index-2fddeca6.html" }`. Прочитать его можно и с бэкенда, и прямо из браузера (`fetch('app-manifest.js', { cache: 'no-store' })`). Альтернативы — взять единственный `index-*.html` в каталоге или отдать имя отдельным методом; выбор за бэкендом.
|
||||||
- Ручной cache-busting (`?v=...`) для JS/CSS/HTML **больше не нужен**: раз URL меняется вместе с содержимым, старый закэшированный файл просто никогда больше не запрашивается, как только известно новое имя.
|
- Ручной cache-busting (`?v=...`) для JS/CSS/HTML **больше не нужен**: раз URL меняется вместе с содержимым, старый закэшированный файл просто никогда больше не запрашивается, как только известно новое имя.
|
||||||
- **После каждого редеплоя старые `index-<hash>.html`/`index-<hash>.js`/`index-<hash>.css` от прошлых версий стоит убирать** из `local/diagram-app/` — они не удаляются сами, и если способ поиска актуального файла — «взять единственный файл по маске», их накопление сломает эту логику.
|
- **После каждого редеплоя старые `index-<hash>.html`/`index-<hash>.js`/`index-<hash>.css` от прошлых версий стоит убирать** из `local/diagram-app/` — они не удаляются сами, и если способ поиска актуального файла — «взять единственный файл по маске», их накопление сломает эту логику.
|
||||||
|
|
||||||
|
|||||||
@@ -219,13 +219,13 @@ Backend не обязан ничего валидировать глубже «
|
|||||||
|
|
||||||
Из этого следует главное отличие от более ранней версии данного документа: **`$appUrl` в PHP-классе (раздел 3) больше нельзя хардкодить как строку** — конкретное имя `index-<hash>.html` меняется при каждой пересборке приложения. Бэкенду нужен свой способ узнавать актуальное имя файла на момент рендера страницы (например, взять единственный `index-*.html` из каталога деплоя, или через отдельный метод/эндпоинт, который вернёт актуальное имя, — механизм на усмотрение бэкенда, здесь не фиксирован, уточняйте на момент внедрения).
|
Из этого следует главное отличие от более ранней версии данного документа: **`$appUrl` в PHP-классе (раздел 3) больше нельзя хардкодить как строку** — конкретное имя `index-<hash>.html` меняется при каждой пересборке приложения. Бэкенду нужен свой способ узнавать актуальное имя файла на момент рендера страницы (например, взять единственный `index-*.html` из каталога деплоя, или через отдельный метод/эндпоинт, который вернёт актуальное имя, — механизм на усмотрение бэкенда, здесь не фиксирован, уточняйте на момент внедрения).
|
||||||
|
|
||||||
**Готовый механизм — `app.json` рядом с билдом.** Тот же плагин `hashIndexHtml` кладёт в `dist/` файл со стабильным, никогда не меняющимся именем:
|
**Готовый механизм — `app-manifest.js` рядом с билдом.** Тот же плагин `hashIndexHtml` кладёт в `dist/` файл со стабильным, никогда не меняющимся именем:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "index": "index-2fddeca6.html" }
|
{ "index": "index-2fddeca6.html" }
|
||||||
```
|
```
|
||||||
|
|
||||||
Его можно прочитать как с бэкенда при рендере страницы, так и на клиенте — `fetch('<каталог>/app.json', { cache: 'no-store' })`, — и уже к полученному имени дописывать `?mode=view`. `no-store` здесь обязателен: имя файла стабильно, значит сам манифест попадает под тот же 30-дневный `Cache-Control`, что и остальная статика. Этим сейчас пользуется portal-front (`src/helpers/chart-builder.ts`), пока у бэка нет своего метода.
|
Его можно прочитать как с бэкенда при рендере страницы, так и на клиенте — `fetch('<каталог>/app-manifest.js', { cache: 'no-store' })`, — и уже к полученному имени дописывать `?mode=view`. `no-store` здесь обязателен: имя файла стабильно, значит сам манифест попадает под тот же 30-дневный `Cache-Control`, что и остальная статика. Этим сейчас пользуется portal-front (`src/helpers/chart-builder.ts`), пока у бэка нет своего метода.
|
||||||
|
|
||||||
Взамен это полностью снимает необходимость в отдельном cache-busting query-параметре (`?v=...`) — раньше он был обязателен и для самого `index.html`, и (до появления хеша у самого файла) для `index.js`/`index.css` внутри него. Теперь URL меняется вместе с содержимым: старый закэшированный `index-OLDHASH.html` просто никогда больше не запрашивается, как только хост узнаёт новое имя — что убирает разом оба уровня кэш-проблемы, описанной в более ранней версии этого раздела.
|
Взамен это полностью снимает необходимость в отдельном cache-busting query-параметре (`?v=...`) — раньше он был обязателен и для самого `index.html`, и (до появления хеша у самого файла) для `index.js`/`index.css` внутри него. Теперь URL меняется вместе с содержимым: старый закэшированный `index-OLDHASH.html` просто никогда больше не запрашивается, как только хост узнаёт новое имя — что убирает разом оба уровня кэш-проблемы, описанной в более ранней версии этого раздела.
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
{ "index": "index-300d7ef5.html" }
|
{ "index": "index-300d7ef5.html" }
|
||||||
```
|
```
|
||||||
|
|
||||||
Портал читает его при открытии модалки — `fetch('app.json', { cache: 'no-store' })`
|
Портал читает его при открытии модалки — `fetch('app-manifest.js', { cache: 'no-store' })`
|
||||||
в `src/helpers/chart-builder.ts` — и уже к полученному имени дописывает
|
в `src/helpers/chart-builder.ts` — и уже к полученному имени дописывает
|
||||||
параметры. Поэтому выкладка конструктора **не требует пересборки портала**.
|
параметры. Поэтому выкладка конструктора **не требует пересборки портала**.
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ cd D:\Development\Work\portal-front; npm run dev
|
|||||||
Проверка, что связка поднята:
|
Проверка, что связка поднята:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
curl.exe -s http://localhost:5173/local/modules/vniigaz.bpmn/assets/app.json
|
curl.exe -s http://localhost:5173/local/modules/vniigaz.bpmn/assets/app-manifest.js
|
||||||
```
|
```
|
||||||
|
|
||||||
Пустой ответ — не поднят `preview` или не собран конструктор.
|
Пустой ответ — не поднят `preview` или не собран конструктор.
|
||||||
@@ -108,7 +108,7 @@ cd D:\Development\Work\portal-front; npm run build
|
|||||||
Проверка после выкладки:
|
Проверка после выкладки:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
curl.exe -s https://ВАШ-ДОМЕН/local/modules/vniigaz.bpmn/assets/app.json
|
curl.exe -s https://ВАШ-ДОМЕН/local/modules/vniigaz.bpmn/assets/app-manifest.js
|
||||||
```
|
```
|
||||||
|
|
||||||
Имя в ответе должно совпадать с реально лежащим файлом. Открыть `index-<hash>.html`
|
Имя в ответе должно совпадать с реально лежащим файлом. Открыть `index-<hash>.html`
|
||||||
@@ -132,7 +132,7 @@ curl.exe -s https://ВАШ-ДОМЕН/local/modules/vniigaz.bpmn/assets/app.json
|
|||||||
состоится, ошибок в консоли не будет.
|
состоится, ошибок в консоли не будет.
|
||||||
|
|
||||||
Второе требование к встраивающей стороне — не писать имя `index-<hash>.html`
|
Второе требование к встраивающей стороне — не писать имя `index-<hash>.html`
|
||||||
строкой, а брать актуальное из `app.json` (или искать единственный `index-*.html`,
|
строкой, а брать актуальное из `app-manifest.js` (или искать единственный `index-*.html`,
|
||||||
но тогда очистка каталога при деплое обязательна). Подробности — в
|
но тогда очистка каталога при деплое обязательна). Подробности — в
|
||||||
`backend-quickstart.md`, раздел 1.
|
`backend-quickstart.md`, раздел 1.
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ curl.exe -s https://ВАШ-ДОМЕН/local/modules/vniigaz.bpmn/assets/app.json
|
|||||||
|
|
||||||
| Что видно | Причина | Что делать |
|
| Что видно | Причина | Что делать |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| «Не удалось загрузить схему» в модалке | манифест не отдался либо не пришёл `diagram:ready` за 10 секунд | проверить `app.json` через curl, затем в Network — дошёл ли `index-<hash>.js` |
|
| «Не удалось загрузить схему» в модалке | манифест не отдался либо не пришёл `diagram:ready` за 10 секунд | проверить `app-manifest.js` через curl, затем в Network — дошёл ли `index-<hash>.js` |
|
||||||
| Пустой iframe, в консоли 404 на `index-<hash>.js` | файлы сборки не докопированы или скопированы не в тот каталог | перекопировать `dist/` целиком |
|
| Пустой iframe, в консоли 404 на `index-<hash>.js` | файлы сборки не докопированы или скопированы не в тот каталог | перекопировать `dist/` целиком |
|
||||||
| Приложение висит на «Загрузка диаграммы…» | `diagram:ready` ушёл не туда: origin хоста не совпал с ожидаемым | для админки — сверить `VITE_BITRIX_ORIGIN` с адресной строкой; для dev — проверить, что конструктор открывается через прокси портала, а не напрямую с 5800 |
|
| Приложение висит на «Загрузка диаграммы…» | `diagram:ready` ушёл не туда: origin хоста не совпал с ожидаемым | для админки — сверить `VITE_BITRIX_ORIGIN` с адресной строкой; для dev — проверить, что конструктор открывается через прокси портала, а не напрямую с 5800 |
|
||||||
| Портал открывает старую версию схемы | в каталоге лежат файлы прошлых сборок, `app.json` указывает на новый, а старый html закеширован браузером | очистить каталог при деплое; хеш в имени сам по себе кэш не победит, если старый файл остаётся на месте |
|
| Портал открывает старую версию схемы | в каталоге лежат файлы прошлых сборок, `app-manifest.js` указывает на новый, а старый html закеширован браузером | очистить каталог при деплое; хеш в имени сам по себе кэш не победит, если старый файл остаётся на месте |
|
||||||
|
|||||||
+179
-99
@@ -26,12 +26,15 @@ import {
|
|||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { Button } from "@consta/uikit/Button";
|
import { Button } from "@consta/uikit/Button";
|
||||||
import { ChoiceGroup } from "@consta/uikit/ChoiceGroup";
|
import { ContextMenu } from "@consta/uikit/ContextMenu";
|
||||||
import { Breadcrumbs } from "@consta/uikit/Breadcrumbs";
|
import { Breadcrumbs } from "@consta/uikit/Breadcrumbs";
|
||||||
import { IconArrowRight } from "@consta/icons/IconArrowRight";
|
import { IconArrowRight } from "@consta/icons/IconArrowRight";
|
||||||
|
import { IconAdd } from "@consta/icons/IconAdd";
|
||||||
|
import { IconQuestion } from "@consta/icons/IconQuestion";
|
||||||
import { nodeTypes, NodesWithChildrenContext, ModeContext } from "./nodes";
|
import { nodeTypes, NodesWithChildrenContext, ModeContext } from "./nodes";
|
||||||
import { NodeInspector } from "./NodeInspector";
|
import { NodeInspector } from "./NodeInspector";
|
||||||
import { EdgeInspector } from "./EdgeInspector";
|
import { EdgeInspector } from "./EdgeInspector";
|
||||||
|
import { HelpPanel } from "./HelpPanel";
|
||||||
import { debounce, serializeDocument } from "./serialize";
|
import { debounce, serializeDocument } from "./serialize";
|
||||||
import type {
|
import type {
|
||||||
DiagramDocument,
|
DiagramDocument,
|
||||||
@@ -70,25 +73,27 @@ const EDGE_DASH_ARRAY: Record<EdgeLineStyle, string | undefined> = {
|
|||||||
const MAX_HISTORY = 50;
|
const MAX_HISTORY = 50;
|
||||||
const TYPING_HISTORY_THROTTLE_MS = 1000;
|
const TYPING_HISTORY_THROTTLE_MS = 1000;
|
||||||
|
|
||||||
const NODE_KINDS: { kind: NodeKind; label: string }[] = [
|
// Группы для выпадающего меню «Добавить элемент» — 11 типов плоским списком
|
||||||
{ kind: "task", label: "+ Действие" },
|
// читаются плохо, поэтому разложены по смыслу нотации.
|
||||||
{ kind: "startEvent", label: "+ Начало" },
|
const NODE_GROUPS: { id: number; label: string }[] = [
|
||||||
{ kind: "intermediateEvent", label: "+ Промеж. событие" },
|
{ id: 1, label: "Действия" },
|
||||||
{ kind: "endEvent", label: "+ Конец" },
|
{ id: 2, label: "События" },
|
||||||
{ kind: "exclusiveGateway", label: "+ Искл. шлюз (XOR)" },
|
{ id: 3, label: "Шлюзы" },
|
||||||
{ kind: "parallelGateway", label: "+ Паралл. шлюз (AND)" },
|
{ id: 4, label: "Прочее" },
|
||||||
{ kind: "inclusiveGateway", label: "+ Вкл. шлюз (OR)" },
|
|
||||||
{ kind: "complexGateway", label: "+ Сложный шлюз" },
|
|
||||||
{ kind: "eventBasedGateway", label: "+ Шлюз по событию" },
|
|
||||||
{ kind: "hint", label: "+ Подсказка" },
|
|
||||||
{ kind: "dataObject", label: "+ Документ" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Stable references — ChoiceGroup's controlled `value` is compared against
|
const NODE_KINDS: { kind: NodeKind; label: string; groupId: number }[] = [
|
||||||
// entries in `items` by identity, so this can't be recreated on every render.
|
{ kind: "task", label: "Действие", groupId: 1 },
|
||||||
const MODE_ITEMS: { label: string; mode: "edit" | "view" }[] = [
|
{ kind: "startEvent", label: "Начало", groupId: 2 },
|
||||||
{ label: "Просмотр", mode: "view" },
|
{ kind: "intermediateEvent", label: "Промежуточное событие", groupId: 2 },
|
||||||
{ label: "Редактирование", mode: "edit" },
|
{ kind: "endEvent", label: "Конец", groupId: 2 },
|
||||||
|
{ kind: "exclusiveGateway", label: "Исключающий шлюз (XOR)", groupId: 3 },
|
||||||
|
{ kind: "parallelGateway", label: "Параллельный шлюз (AND)", groupId: 3 },
|
||||||
|
{ kind: "inclusiveGateway", label: "Включающий шлюз (OR)", groupId: 3 },
|
||||||
|
{ kind: "complexGateway", label: "Сложный шлюз", groupId: 3 },
|
||||||
|
{ kind: "eventBasedGateway", label: "Шлюз по событию", groupId: 3 },
|
||||||
|
{ kind: "hint", label: "Подсказка", groupId: 4 },
|
||||||
|
{ kind: "dataObject", label: "Документ", groupId: 4 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const DEFAULT_NODE_SIZE: Record<NodeKind, { width: number; height: number }> = {
|
const DEFAULT_NODE_SIZE: Record<NodeKind, { width: number; height: number }> = {
|
||||||
@@ -205,7 +210,11 @@ function isEditableTarget(target: EventTarget | null): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DiagramCanvas({ initialValue, onChange, lockedMode }: DiagramEditorProps) {
|
function DiagramCanvas({
|
||||||
|
initialValue,
|
||||||
|
onChange,
|
||||||
|
lockedMode,
|
||||||
|
}: DiagramEditorProps) {
|
||||||
const [nodes, setNodes] = useState<Node[]>(() => toRfNodes(initialValue));
|
const [nodes, setNodes] = useState<Node[]>(() => toRfNodes(initialValue));
|
||||||
const [edges, setEdges] = useState<Edge[]>(() => toRfEdges(initialValue));
|
const [edges, setEdges] = useState<Edge[]>(() => toRfEdges(initialValue));
|
||||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||||
@@ -214,6 +223,9 @@ function DiagramCanvas({ initialValue, onChange, lockedMode }: DiagramEditorProp
|
|||||||
const [selectedMode, setSelectedMode] = useState<"edit" | "view">("edit");
|
const [selectedMode, setSelectedMode] = useState<"edit" | "view">("edit");
|
||||||
const mode = lockedMode ?? selectedMode;
|
const mode = lockedMode ?? selectedMode;
|
||||||
const [alignmentGuides, setAlignmentGuides] = useState<AlignmentGuide[]>([]);
|
const [alignmentGuides, setAlignmentGuides] = useState<AlignmentGuide[]>([]);
|
||||||
|
const [isAddMenuOpen, setIsAddMenuOpen] = useState(false);
|
||||||
|
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||||
|
const addButtonRef = useRef<HTMLDivElement>(null);
|
||||||
const nodeCounter = useRef(0);
|
const nodeCounter = useRef(0);
|
||||||
const historyRef = useRef<{ nodes: Node[]; edges: Edge[] }[]>([]);
|
const historyRef = useRef<{ nodes: Node[]; edges: Edge[] }[]>([]);
|
||||||
const redoRef = useRef<{ nodes: Node[]; edges: Edge[] }[]>([]);
|
const redoRef = useRef<{ nodes: Node[]; edges: Edge[] }[]>([]);
|
||||||
@@ -490,24 +502,38 @@ function DiagramCanvas({ initialValue, onChange, lockedMode }: DiagramEditorProp
|
|||||||
[nodes, edges, emitChange, pushHistory],
|
[nodes, edges, emitChange, pushHistory],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onNodeClick: NodeMouseHandler = useCallback((_event, node) => {
|
// React Flow гасит всплытие событий на узлах и связях, поэтому глобальные
|
||||||
setSelectedNodeId(node.id);
|
// «клик снаружи» у выпадающих меню на них не срабатывают — закрываем сами
|
||||||
setSelectedEdgeId(null);
|
// из каждого обработчика холста.
|
||||||
|
const closePopups = useCallback(() => {
|
||||||
|
setIsAddMenuOpen(false);
|
||||||
|
setIsHelpOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onNodeClick: NodeMouseHandler = useCallback(
|
||||||
|
(_event, node) => {
|
||||||
|
closePopups();
|
||||||
|
setSelectedNodeId(node.id);
|
||||||
|
setSelectedEdgeId(null);
|
||||||
|
},
|
||||||
|
[closePopups],
|
||||||
|
);
|
||||||
|
|
||||||
const onEdgeClick: EdgeMouseHandler = useCallback(
|
const onEdgeClick: EdgeMouseHandler = useCallback(
|
||||||
(_event, edge) => {
|
(_event, edge) => {
|
||||||
if (mode !== "edit") return;
|
if (mode !== "edit") return;
|
||||||
|
closePopups();
|
||||||
setSelectedEdgeId(edge.id);
|
setSelectedEdgeId(edge.id);
|
||||||
setSelectedNodeId(null);
|
setSelectedNodeId(null);
|
||||||
},
|
},
|
||||||
[mode],
|
[mode, closePopups],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onPaneClick = useCallback(() => {
|
const onPaneClick = useCallback(() => {
|
||||||
|
closePopups();
|
||||||
setSelectedNodeId(null);
|
setSelectedNodeId(null);
|
||||||
setSelectedEdgeId(null);
|
setSelectedEdgeId(null);
|
||||||
}, []);
|
}, [closePopups]);
|
||||||
|
|
||||||
const updateSelectedNodeData = useCallback(
|
const updateSelectedNodeData = useCallback(
|
||||||
(patch: Partial<DiagramNodeData>) => {
|
(patch: Partial<DiagramNodeData>) => {
|
||||||
@@ -719,6 +745,7 @@ function DiagramCanvas({ initialValue, onChange, lockedMode }: DiagramEditorProp
|
|||||||
const next = edges.map((e) =>
|
const next = edges.map((e) =>
|
||||||
e.id === selectedEdgeId ? { ...e, zIndex: z } : e,
|
e.id === selectedEdgeId ? { ...e, zIndex: z } : e,
|
||||||
);
|
);
|
||||||
|
console.log();
|
||||||
setEdges(next);
|
setEdges(next);
|
||||||
emitChange(nodes, next);
|
emitChange(nodes, next);
|
||||||
}, [selectedEdgeId, nodes, edges, emitChange, pushHistory, minZIndex]);
|
}, [selectedEdgeId, nodes, edges, emitChange, pushHistory, minZIndex]);
|
||||||
@@ -756,15 +783,33 @@ function DiagramCanvas({ initialValue, onChange, lockedMode }: DiagramEditorProp
|
|||||||
edges.find((edge) => edge.id === selectedEdgeId)
|
edges.find((edge) => edge.id === selectedEdgeId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// В режиме просмотра панель — не постоянная часть интерфейса, а всплывающая
|
// Панель — всплывающая деталь выбранной фигуры, одинаково в обоих режимах:
|
||||||
// деталь выбранной фигуры: без выбора её нечем наполнить (подсказки про
|
// без выбора её нечем наполнить. Она лежит оверлеем поверх холста, а не
|
||||||
// «Дублировать»/«Удалить» там неуместны), поэтому холст занимает всю ширину.
|
// сжимает его — поэтому камеру при открытии трогать не нужно вообще:
|
||||||
// В режиме редактирования панель нужна всегда — там она несёт инструменты,
|
// ничего никуда не уезжает, и вид остаётся ровно там, где его оставил
|
||||||
// а не только описание выбранного узла.
|
// пользователь.
|
||||||
const isAsideVisible = mode === "edit" || Boolean(selectedNode || selectedEdge);
|
const isAsideVisible = Boolean(selectedNode || selectedEdge);
|
||||||
|
|
||||||
|
// «Клик вне панели закрывает её». Клики по самим элементам схемы сюда не
|
||||||
|
// попадают: они переключают панель на выбранный элемент, а не закрывают её
|
||||||
|
// (иначе просмотр нескольких элементов подряд требовал бы двойных кликов).
|
||||||
|
//
|
||||||
|
// Обработчик висит на всём shell и срабатывает по всплытию — то есть уже
|
||||||
|
// ПОСЛЕ onNodeClick/onEdgeClick. Пропустить здесь элемент схемы означает
|
||||||
|
// мгновенно сбросить только что выставленное выделение, поэтому исключения
|
||||||
|
// должны покрывать и узлы, и связи (у связей кликабельны path и подпись —
|
||||||
|
// все они лежат внутри группы .react-flow__edge).
|
||||||
|
const onShellClick = useCallback((event: React.MouseEvent) => {
|
||||||
|
const target = event.target as Element;
|
||||||
|
if (target.closest(".diagram-aside")) return;
|
||||||
|
if (target.closest(".react-flow__node")) return;
|
||||||
|
if (target.closest(".react-flow__edge")) return;
|
||||||
|
setSelectedNodeId(null);
|
||||||
|
setSelectedEdgeId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="diagram-shell">
|
<div className="diagram-shell" onClick={onShellClick}>
|
||||||
<div className="diagram-canvas" ref={canvasWrapperRef}>
|
<div className="diagram-canvas" ref={canvasWrapperRef}>
|
||||||
<NodesWithChildrenContext.Provider value={nodesWithChildren}>
|
<NodesWithChildrenContext.Provider value={nodesWithChildren}>
|
||||||
<ModeContext.Provider value={mode}>
|
<ModeContext.Provider value={mode}>
|
||||||
@@ -849,32 +894,83 @@ function DiagramCanvas({ initialValue, onChange, lockedMode }: DiagramEditorProp
|
|||||||
</ControlButton>
|
</ControlButton>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{/*
|
||||||
|
Переключатель режима живёт здесь, а не отдельной панелью:
|
||||||
|
скрыт, когда режим зафиксирован хостом (`?mode=view`) —
|
||||||
|
тогда переключать нечего. `title` служит подсказкой при
|
||||||
|
наведении, как и у остальных кнопок этой панели.
|
||||||
|
*/}
|
||||||
|
{!lockedMode && (
|
||||||
|
<ControlButton
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedMode(mode === "edit" ? "view" : "edit")
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
mode === "edit" ?
|
||||||
|
"Режим просмотра: схема только для чтения"
|
||||||
|
: "Вернуться к редактированию"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{mode === "edit" ? "👁" : "✎"}
|
||||||
|
</ControlButton>
|
||||||
|
)}
|
||||||
</Controls>
|
</Controls>
|
||||||
{mode === "edit" && <MiniMap />}
|
{mode === "edit" && <MiniMap />}
|
||||||
{!lockedMode && (
|
{mode === "edit" && (
|
||||||
<Panel position="top-right" className="diagram-mode-toggle">
|
<Panel position="top-left" className="diagram-toolbar">
|
||||||
<ChoiceGroup
|
<div ref={addButtonRef}>
|
||||||
size="xs"
|
<Button
|
||||||
name="diagram-mode"
|
size="s"
|
||||||
items={MODE_ITEMS}
|
view="primary"
|
||||||
value={MODE_ITEMS.find((item) => item.mode === mode) ?? null}
|
label="Добавить элемент"
|
||||||
onChange={(item) => setSelectedMode(item.mode)}
|
iconLeft={IconAdd}
|
||||||
|
onClick={() => setIsAddMenuOpen((open) => !open)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ContextMenu
|
||||||
|
isOpen={isAddMenuOpen}
|
||||||
|
items={NODE_KINDS}
|
||||||
|
groups={NODE_GROUPS}
|
||||||
|
getItemKey={(item) => item.kind}
|
||||||
|
getItemLabel={(item) => item.label}
|
||||||
|
getItemGroupId={(item) => item.groupId}
|
||||||
|
getGroupId={(group) => group.id}
|
||||||
|
getGroupLabel={(group) => group.label}
|
||||||
|
anchorRef={addButtonRef as React.RefObject<HTMLElement>}
|
||||||
|
direction="downStartLeft"
|
||||||
|
size="s"
|
||||||
|
onItemClick={(item) => {
|
||||||
|
addNode(item.kind);
|
||||||
|
setIsAddMenuOpen(false);
|
||||||
|
}}
|
||||||
|
onClickOutside={() => setIsAddMenuOpen(false)}
|
||||||
|
onEsc={() => setIsAddMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
{mode === "edit" && (
|
<Panel position="top-right" className="diagram-help">
|
||||||
<Panel position="top-left" className="diagram-toolbar">
|
<Button
|
||||||
{NODE_KINDS.map(({ kind, label }) => (
|
size="s"
|
||||||
<Button
|
view="ghost"
|
||||||
key={kind}
|
form="round"
|
||||||
size="xs"
|
onlyIcon
|
||||||
view="ghost"
|
iconLeft={IconQuestion}
|
||||||
label={label}
|
title="Справка по работе с конструктором"
|
||||||
onClick={() => addNode(kind)}
|
aria-label="Справка по работе с конструктором"
|
||||||
/>
|
onClick={() => {
|
||||||
))}
|
setIsAddMenuOpen(false);
|
||||||
</Panel>
|
setIsHelpOpen((open) => !open);
|
||||||
)}
|
}}
|
||||||
|
/>
|
||||||
|
{isHelpOpen && (
|
||||||
|
<HelpPanel mode={mode} canSwitchMode={!lockedMode} />
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
{/*
|
||||||
|
Крошки прячутся, пока открыта боковая панель: холст сужается,
|
||||||
|
и они наезжают на кнопки «Добавить элемент» и «?» по краям.
|
||||||
|
*/}
|
||||||
|
{!isAsideVisible && (
|
||||||
<Panel position="top-center" className="diagram-breadcrumbs">
|
<Panel position="top-center" className="diagram-breadcrumbs">
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -894,14 +990,14 @@ function DiagramCanvas({ initialValue, onChange, lockedMode }: DiagramEditorProp
|
|||||||
getItemOnClick={(item) => item.onClick}
|
getItemOnClick={(item) => item.onClick}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
)}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</ModeContext.Provider>
|
</ModeContext.Provider>
|
||||||
</NodesWithChildrenContext.Provider>
|
</NodesWithChildrenContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAsideVisible && (
|
{isAsideVisible && (
|
||||||
<aside className="diagram-aside">
|
<aside className="diagram-aside">
|
||||||
{mode === "view" && (
|
|
||||||
<div className="diagram-aside__close">
|
<div className="diagram-aside__close">
|
||||||
<Button
|
<Button
|
||||||
size="s"
|
size="s"
|
||||||
@@ -914,51 +1010,35 @@ function DiagramCanvas({ initialValue, onChange, lockedMode }: DiagramEditorProp
|
|||||||
onClick={onPaneClick}
|
onClick={onPaneClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{selectedNode ?
|
||||||
{selectedNode ?
|
<NodeInspector
|
||||||
<NodeInspector
|
nodeId={selectedNode.id}
|
||||||
nodeId={selectedNode.id}
|
nodeType={(selectedNode.type ?? "task") as NodeKind}
|
||||||
nodeType={(selectedNode.type ?? "task") as NodeKind}
|
data={selectedNode.data as DiagramNodeData}
|
||||||
data={selectedNode.data as DiagramNodeData}
|
childCount={selectedNodeChildCount}
|
||||||
childCount={selectedNodeChildCount}
|
mode={mode}
|
||||||
mode={mode}
|
onChange={updateSelectedNodeData}
|
||||||
onChange={updateSelectedNodeData}
|
onEnterSubProcess={enterSubProcess}
|
||||||
onEnterSubProcess={enterSubProcess}
|
onDuplicate={duplicateSelectedNode}
|
||||||
onDuplicate={duplicateSelectedNode}
|
onDelete={deleteSelectedNode}
|
||||||
onDelete={deleteSelectedNode}
|
onBringToFront={bringSelectedNodeToFront}
|
||||||
onBringToFront={bringSelectedNodeToFront}
|
onSendToBack={sendSelectedNodeToBack}
|
||||||
onSendToBack={sendSelectedNodeToBack}
|
/>
|
||||||
/>
|
: selectedEdge ?
|
||||||
: selectedEdge ?
|
<EdgeInspector
|
||||||
<EdgeInspector
|
edgeId={selectedEdge.id}
|
||||||
edgeId={selectedEdge.id}
|
label={
|
||||||
label={
|
typeof selectedEdge.label === "string" ? selectedEdge.label : ""
|
||||||
typeof selectedEdge.label === "string" ? selectedEdge.label : ""
|
}
|
||||||
}
|
data={(selectedEdge.data as DiagramEdgeData) ?? {}}
|
||||||
data={(selectedEdge.data as DiagramEdgeData) ?? {}}
|
onLabelChange={updateSelectedEdgeLabel}
|
||||||
onLabelChange={updateSelectedEdgeLabel}
|
onDataChange={updateSelectedEdgeData}
|
||||||
onDataChange={updateSelectedEdgeData}
|
onDelete={deleteSelectedEdge}
|
||||||
onDelete={deleteSelectedEdge}
|
onBringToFront={bringSelectedEdgeToFront}
|
||||||
onBringToFront={bringSelectedEdgeToFront}
|
onSendToBack={sendSelectedEdgeToBack}
|
||||||
onSendToBack={sendSelectedEdgeToBack}
|
/>
|
||||||
/>
|
: null}
|
||||||
: <p className="diagram-aside__hint">
|
</aside>
|
||||||
{mode === "edit" ?
|
|
||||||
"Выберите узел или связь на схеме — появятся кнопки «Дублировать»/«Удалить» и поля данных."
|
|
||||||
: "Кликните по узлу, чтобы увидеть описание. Если у узла есть вложенный процесс, появится кнопка перехода."
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
{mode === "edit" && (
|
|
||||||
<details className="diagram-aside__json">
|
|
||||||
<summary>Текущий doc (для демо)</summary>
|
|
||||||
<pre>
|
|
||||||
{JSON.stringify(serializeDocument(nodes, edges), null, 2)}
|
|
||||||
</pre>
|
|
||||||
</details>
|
|
||||||
)}
|
|
||||||
</aside>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -94,9 +94,6 @@ export function EdgeInspector({
|
|||||||
<Text size="s" weight="semibold">
|
<Text size="s" weight="semibold">
|
||||||
Поток управления (Sequence Flow)
|
Поток управления (Sequence Flow)
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" view="secondary" font="mono">
|
|
||||||
{edgeId}
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="node-inspector__toolbar">
|
<div className="node-inspector__toolbar">
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { Text } from "@consta/uikit/Text";
|
||||||
|
|
||||||
|
/** Раздел справки: заголовок + список строк «что» → «как». */
|
||||||
|
interface HelpSection {
|
||||||
|
title: string;
|
||||||
|
rows: { term: string; description: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EDIT_SECTIONS: HelpSection[] = [
|
||||||
|
{
|
||||||
|
title: "Работа со схемой",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
term: "Выбор и редактирование",
|
||||||
|
description:
|
||||||
|
"Клик по фигуре или связи — справа откроется панель со свойствами: название, описание, цвет, размер текста. Закрыть панель — стрелкой в её углу или кликом по свободному месту холста.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Добавление",
|
||||||
|
description:
|
||||||
|
"Кнопка «Добавить элемент» слева сверху — фигура появится в центре видимой области.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Соединение фигур",
|
||||||
|
description:
|
||||||
|
"Навести курсор на фигуру и потянуть от точки на её границе к другой фигуре. Точка есть с каждой из четырёх сторон.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Вложенные процессы",
|
||||||
|
description:
|
||||||
|
"Выбрать действие и нажать «Войти в под-процесс →» в боковой панели — внутри можно собрать детальную схему этого шага. Вернуться на уровень выше — через «хлебные крошки» сверху (они появляются, когда боковая панель закрыта). У фигур с вложением есть значок ⇒.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Выравнивание",
|
||||||
|
description:
|
||||||
|
"При перетаскивании фигура сама примагничивается к центрам соседних по горизонтали и вертикали — по линиям видно, к чему именно.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Горячие клавиши",
|
||||||
|
rows: [
|
||||||
|
{ term: "Ctrl + Z", description: "Отменить последнее действие" },
|
||||||
|
{
|
||||||
|
term: "Ctrl + C / Ctrl + V",
|
||||||
|
description: "Копировать и вставить выбранную фигуру",
|
||||||
|
},
|
||||||
|
{ term: "Delete", description: "Удалить выбранную фигуру или связь" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Режим просмотра — только то, что в нём действительно доступно: фигуры
|
||||||
|
* нельзя двигать, создавать, соединять и удалять, горячие клавиши и
|
||||||
|
* отмена отключены, связи не выделяются. Поэтому здесь нет ни слова про
|
||||||
|
* перетаскивание, добавление и правку — иначе справка обещала бы то,
|
||||||
|
* чего в этом режиме нет.
|
||||||
|
*/
|
||||||
|
const VIEW_SECTIONS: HelpSection[] = [
|
||||||
|
{
|
||||||
|
title: "Просмотр схемы",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
term: "Описание шага",
|
||||||
|
description:
|
||||||
|
"Клик по фигуре — справа откроется панель с её названием, описанием, ответственным и длительностью, если они заданы. Закрыть панель — стрелкой в её углу или кликом по свободному месту холста.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Вложенные процессы",
|
||||||
|
description:
|
||||||
|
"У фигур со значком ⇒ внутри есть своя схема: выберите такую фигуру и нажмите «Открыть вложенный процесс →». Вернуться на уровень выше — через «хлебные крошки» сверху (они видны, когда боковая панель закрыта).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Масштаб и навигация",
|
||||||
|
description:
|
||||||
|
"Кнопки «+» и «−» в панели слева внизу, либо колесо мыши. Кнопка «⛶» вписывает всю схему в экран. Чтобы сдвинуть схему, зажмите пустое место холста и ведите мышью.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const NOTATION_SECTION: HelpSection = {
|
||||||
|
title: "Элементы нотации BPMN",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
term: "Действие (Task)",
|
||||||
|
description:
|
||||||
|
"Шаг процесса — то, что делается. Подтип задаёт характер работы: пользователем, сервисом, скриптом, вручную, по бизнес-правилам, отправка или получение сообщения.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "События (Event)",
|
||||||
|
description:
|
||||||
|
"Круги: начальное — с чего процесс стартует, промежуточное — что происходит по ходу, конечное — чем завершается. Иконка внутри показывает триггер: сообщение, таймер, условие, сигнал, ошибка и другие. Для каждого вида события доступны только допустимые триггеры.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Шлюзы (Gateway)",
|
||||||
|
description:
|
||||||
|
"Ромбы — точки ветвления. XOR (✕): ровно один путь из нескольких. AND (+): все пути одновременно. OR (○): один или несколько сразу. Сложный: нестандартная логика, описывается текстом. По событию: путь выбирается тем, какое событие произойдёт первым.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Документ (Data Object)",
|
||||||
|
description:
|
||||||
|
"Вход или выход шага — документ, файл, данные. Соединяется с действием пунктирной линией (это связь данных, а не поток управления).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: "Подсказка",
|
||||||
|
description:
|
||||||
|
"Не элемент нотации, а поясняющая заметка: цветной блок с текстом, который размещается рядом с фигурами. Связи к нему не подключаются.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HelpPanelProps {
|
||||||
|
mode: "edit" | "view";
|
||||||
|
/**
|
||||||
|
* Можно ли вернуться к редактированию кнопкой с карандашом. Когда режим
|
||||||
|
* зафиксирован хостом (`?mode=view`), кнопки нет — и обещать её нельзя.
|
||||||
|
*/
|
||||||
|
canSwitchMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Справка — выпадающая панель под кнопкой «?», а не модалка во весь экран:
|
||||||
|
* приложение живёт внутри iframe на странице портала, и оверлей на весь
|
||||||
|
* viewport перекрыл бы только сам iframe, выглядя оторванным от страницы.
|
||||||
|
* Рендерится обычным потоком внутри Panel холста, без портала в body —
|
||||||
|
* так панель гарантированно остаётся в границах приложения.
|
||||||
|
*/
|
||||||
|
export function HelpPanel({ mode, canSwitchMode }: HelpPanelProps) {
|
||||||
|
const modeSections = mode === "edit" ? EDIT_SECTIONS : VIEW_SECTIONS;
|
||||||
|
|
||||||
|
// Про режимы рассказываем только в редактировании — там это осмысленный
|
||||||
|
// переход «сейчас правлю, хочу посмотреть». В просмотре не рассказываем
|
||||||
|
// вовсе: читателю схемы незачем знать про режим редактирования, а когда
|
||||||
|
// просмотр зафиксирован хостом (`?mode=view`), выйти из него и нельзя.
|
||||||
|
const modeSwitchSection: HelpSection[] =
|
||||||
|
mode === "edit" && canSwitchMode ?
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: "Режимы",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
term: "Перейти к просмотру",
|
||||||
|
description:
|
||||||
|
"Кнопка с глазом в панели слева внизу — схема станет только для чтения: фигуры нельзя двигать, создавать и удалять.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const sections = [...modeSections, ...modeSwitchSection, NOTATION_SECTION];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="help-panel">
|
||||||
|
{sections.map((section) => (
|
||||||
|
<div key={section.title} className="help-panel__section">
|
||||||
|
<Text size="xs" weight="semibold" className="help-panel__section-title">
|
||||||
|
{section.title}
|
||||||
|
</Text>
|
||||||
|
<dl className="help-panel__list">
|
||||||
|
{section.rows.map((row) => (
|
||||||
|
<div key={row.term} className="help-panel__row">
|
||||||
|
<dt>
|
||||||
|
<Text size="2xs" weight="semibold">
|
||||||
|
{row.term}
|
||||||
|
</Text>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<Text size="2xs" view="secondary">
|
||||||
|
{row.description}
|
||||||
|
</Text>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -200,9 +200,6 @@ export function NodeInspector({
|
|||||||
<Text size="s" weight="semibold">
|
<Text size="s" weight="semibold">
|
||||||
{TYPE_LABELS[nodeType]}
|
{TYPE_LABELS[nodeType]}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" view="secondary" font="mono">
|
|
||||||
{nodeId}
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="node-inspector__toolbar">
|
<div className="node-inspector__toolbar">
|
||||||
|
|||||||
+74
-7
@@ -1,19 +1,69 @@
|
|||||||
.diagram-shell {
|
.diagram-shell {
|
||||||
display: flex;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diagram-canvas {
|
.diagram-canvas {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
min-width: 0;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diagram-toolbar {
|
.diagram-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
max-width: 280px;
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Кнопка справки в правом верхнем углу холста. Раньше это место занимал
|
||||||
|
* переключатель режимов — он переехал в панель управления слева внизу,
|
||||||
|
* к остальным кнопкам работы с холстом.
|
||||||
|
*/
|
||||||
|
.diagram-help {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Выпадающая справка под кнопкой «?». Не модалка и не портал в body:
|
||||||
|
* приложение работает внутри iframe, и оверлей во весь экран перекрыл бы
|
||||||
|
* только сам iframe — панель должна оставаться в границах приложения.
|
||||||
|
*/
|
||||||
|
.help-panel {
|
||||||
|
width: min(420px, 60vw);
|
||||||
|
max-height: min(460px, 70vh);
|
||||||
|
overflow: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 4px 14px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: canvas;
|
||||||
|
border: 1px solid color-mix(in srgb, canvastext 15%, transparent);
|
||||||
|
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-panel__section-title {
|
||||||
|
display: block;
|
||||||
|
margin: 12px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-panel__list {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-panel__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(90px, 130px) 1fr;
|
||||||
|
gap: 2px 12px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-panel__row dd {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -162,10 +212,27 @@
|
|||||||
clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 14px, 100% 100%, 0 100%);
|
clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 14px, 100% 100%, 0 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Плавающая карточка поверх холста, а не колонка рядом с ним. Так холст не
|
||||||
|
* меняет ширину при открытии панели: фигуры остаются на своих экранных
|
||||||
|
* местах, и камеру подстраивать не нужно.
|
||||||
|
*
|
||||||
|
* Отступ сверху уводит карточку из-под кнопки справки в правом верхнем углу —
|
||||||
|
* иначе оверлей перекрывал бы её и справку нельзя было бы открыть, пока
|
||||||
|
* выбрана фигура.
|
||||||
|
*/
|
||||||
.diagram-aside {
|
.diagram-aside {
|
||||||
|
position: absolute;
|
||||||
|
top: 52px;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
width: 340px;
|
width: 340px;
|
||||||
flex-shrink: 0;
|
z-index: 5;
|
||||||
border-left: 1px solid color-mix(in srgb, currentcolor 20%, transparent);
|
box-sizing: border-box;
|
||||||
|
background: canvas;
|
||||||
|
border: 1px solid color-mix(in srgb, canvastext 15%, transparent);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+1
-1
@@ -186,7 +186,7 @@ function SubProcessBadge({ id }: { id: string }) {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="diagram-node__badge"
|
className="diagram-node__badge"
|
||||||
title="Есть вложенные узлы — двойной клик, чтобы открыть"
|
title="Есть вложенные узлы — выберите фигуру, чтобы открыть их из боковой панели"
|
||||||
>
|
>
|
||||||
⇒
|
⇒
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+8
-1
@@ -15,7 +15,14 @@ import react from '@vitejs/plugin-react'
|
|||||||
// so this hashes and renames the file directly on disk after the whole build
|
// so this hashes and renames the file directly on disk after the whole build
|
||||||
// has finished writing it, rather than fighting the bundler's own emit step.
|
// has finished writing it, rather than fighting the bundler's own emit step.
|
||||||
// Stable entry point for hosts: never hashed, always present next to the build.
|
// Stable entry point for hosts: never hashed, always present next to the build.
|
||||||
const MANIFEST_FILE_NAME = 'app.json'
|
//
|
||||||
|
// Content is JSON, but the extension is .js on purpose: the Bitrix stand's
|
||||||
|
// nginx serves static straight from disk only for a fixed extension list
|
||||||
|
// (js|css|html|svg|png|...), and `json` is not in it — a .json file falls
|
||||||
|
// through to the Bitrix PHP router and comes back as a 404 HTML page
|
||||||
|
// (verified on the stand). `.js` is the one extension guaranteed to be served
|
||||||
|
// anywhere this build is deployed, since the build itself depends on it.
|
||||||
|
const MANIFEST_FILE_NAME = 'app-manifest.js'
|
||||||
|
|
||||||
function hashIndexHtml(): Plugin {
|
function hashIndexHtml(): Plugin {
|
||||||
let outDir = 'dist'
|
let outDir = 'dist'
|
||||||
|
|||||||
Reference in New Issue
Block a user