duplicate.test.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. import React from "react";
  2. import { pointFrom } from "@excalidraw/math";
  3. import {
  4. FONT_FAMILY,
  5. ORIG_ID,
  6. ROUNDNESS,
  7. isPrimitive,
  8. } from "@excalidraw/common";
  9. import { Excalidraw } from "@excalidraw/excalidraw";
  10. import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
  11. import { API } from "@excalidraw/excalidraw/tests/helpers/api";
  12. import { Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
  13. import {
  14. act,
  15. assertElements,
  16. getCloneByOrigId,
  17. render,
  18. } from "@excalidraw/excalidraw/tests/test-utils";
  19. import type { LocalPoint } from "@excalidraw/math";
  20. import { mutateElement } from "../src/mutateElement";
  21. import { duplicateElement, duplicateElements } from "../src/duplicate";
  22. import type { ExcalidrawLinearElement } from "../src/types";
  23. const { h } = window;
  24. const mouse = new Pointer("mouse");
  25. const assertCloneObjects = (source: any, clone: any) => {
  26. for (const key in clone) {
  27. if (clone.hasOwnProperty(key) && !isPrimitive(clone[key])) {
  28. expect(clone[key]).not.toBe(source[key]);
  29. if (source[key]) {
  30. assertCloneObjects(source[key], clone[key]);
  31. }
  32. }
  33. }
  34. };
  35. describe("duplicating single elements", () => {
  36. it("clones arrow element", () => {
  37. const element = API.createElement({
  38. type: "arrow",
  39. x: 0,
  40. y: 0,
  41. strokeColor: "#000000",
  42. backgroundColor: "transparent",
  43. fillStyle: "hachure",
  44. strokeWidth: 1,
  45. strokeStyle: "solid",
  46. roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
  47. roughness: 1,
  48. opacity: 100,
  49. });
  50. // @ts-ignore
  51. element.__proto__ = { hello: "world" };
  52. mutateElement(element, {
  53. points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
  54. });
  55. const copy = duplicateElement(null, new Map(), element, undefined, true);
  56. assertCloneObjects(element, copy);
  57. // assert we clone the object's prototype
  58. // @ts-ignore
  59. expect(copy.__proto__).toEqual({ hello: "world" });
  60. expect(copy.hasOwnProperty("hello")).toBe(false);
  61. expect(copy.points).not.toBe(element.points);
  62. expect(copy).not.toHaveProperty("shape");
  63. expect(copy.id).not.toBe(element.id);
  64. expect(typeof copy.id).toBe("string");
  65. expect(copy.seed).not.toBe(element.seed);
  66. expect(typeof copy.seed).toBe("number");
  67. expect(copy).toEqual({
  68. ...element,
  69. id: copy.id,
  70. seed: copy.seed,
  71. version: copy.version,
  72. versionNonce: copy.versionNonce,
  73. });
  74. });
  75. it("clones text element", () => {
  76. const element = API.createElement({
  77. type: "text",
  78. x: 0,
  79. y: 0,
  80. strokeColor: "#000000",
  81. backgroundColor: "transparent",
  82. fillStyle: "hachure",
  83. strokeWidth: 1,
  84. strokeStyle: "solid",
  85. roundness: null,
  86. roughness: 1,
  87. opacity: 100,
  88. text: "hello",
  89. fontSize: 20,
  90. fontFamily: FONT_FAMILY.Virgil,
  91. textAlign: "left",
  92. verticalAlign: "top",
  93. });
  94. const copy = duplicateElement(null, new Map(), element);
  95. assertCloneObjects(element, copy);
  96. expect(copy).not.toHaveProperty("points");
  97. expect(copy).not.toHaveProperty("shape");
  98. expect(copy.id).not.toBe(element.id);
  99. expect(typeof copy.id).toBe("string");
  100. expect(typeof copy.seed).toBe("number");
  101. });
  102. });
  103. describe("duplicating multiple elements", () => {
  104. it("duplicateElements should clone bindings", () => {
  105. const rectangle1 = API.createElement({
  106. type: "rectangle",
  107. id: "rectangle1",
  108. boundElements: [
  109. { id: "arrow1", type: "arrow" },
  110. { id: "arrow2", type: "arrow" },
  111. { id: "text1", type: "text" },
  112. ],
  113. });
  114. const text1 = API.createElement({
  115. type: "text",
  116. id: "text1",
  117. containerId: "rectangle1",
  118. });
  119. const arrow1 = API.createElement({
  120. type: "arrow",
  121. id: "arrow1",
  122. startBinding: {
  123. elementId: "rectangle1",
  124. focus: 0.2,
  125. gap: 7,
  126. fixedPoint: [0.5, 1],
  127. },
  128. });
  129. const arrow2 = API.createElement({
  130. type: "arrow",
  131. id: "arrow2",
  132. endBinding: {
  133. elementId: "rectangle1",
  134. focus: 0.2,
  135. gap: 7,
  136. fixedPoint: [0.5, 1],
  137. },
  138. boundElements: [{ id: "text2", type: "text" }],
  139. });
  140. const text2 = API.createElement({
  141. type: "text",
  142. id: "text2",
  143. containerId: "arrow2",
  144. });
  145. // -------------------------------------------------------------------------
  146. const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
  147. const { newElements: clonedElements } = duplicateElements({
  148. type: "everything",
  149. elements: origElements,
  150. });
  151. // generic id in-equality checks
  152. // --------------------------------------------------------------------------
  153. expect(origElements.map((e) => e.type)).toEqual(
  154. clonedElements.map((e) => e.type),
  155. );
  156. origElements.forEach((origElement, idx) => {
  157. const clonedElement = clonedElements[idx];
  158. expect(origElement).toEqual(
  159. expect.objectContaining({
  160. id: expect.not.stringMatching(clonedElement.id),
  161. type: clonedElement.type,
  162. }),
  163. );
  164. if ("containerId" in origElement) {
  165. expect(origElement.containerId).not.toBe(
  166. (clonedElement as any).containerId,
  167. );
  168. }
  169. if ("endBinding" in origElement) {
  170. if (origElement.endBinding) {
  171. expect(origElement.endBinding.elementId).not.toBe(
  172. (clonedElement as any).endBinding?.elementId,
  173. );
  174. } else {
  175. expect((clonedElement as any).endBinding).toBeNull();
  176. }
  177. }
  178. if ("startBinding" in origElement) {
  179. if (origElement.startBinding) {
  180. expect(origElement.startBinding.elementId).not.toBe(
  181. (clonedElement as any).startBinding?.elementId,
  182. );
  183. } else {
  184. expect((clonedElement as any).startBinding).toBeNull();
  185. }
  186. }
  187. });
  188. // --------------------------------------------------------------------------
  189. const clonedArrows = clonedElements.filter(
  190. (e) => e.type === "arrow",
  191. ) as ExcalidrawLinearElement[];
  192. const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
  193. clonedElements as any as typeof origElements;
  194. expect(clonedText1.containerId).toBe(clonedRectangle.id);
  195. expect(
  196. clonedRectangle.boundElements!.find((e) => e.id === clonedText1.id),
  197. ).toEqual(
  198. expect.objectContaining({
  199. id: clonedText1.id,
  200. type: clonedText1.type,
  201. }),
  202. );
  203. expect(clonedRectangle.type).toBe("rectangle");
  204. clonedArrows.forEach((arrow) => {
  205. expect(
  206. clonedRectangle.boundElements!.find((e) => e.id === arrow.id),
  207. ).toEqual(
  208. expect.objectContaining({
  209. id: arrow.id,
  210. type: arrow.type,
  211. }),
  212. );
  213. if (arrow.endBinding) {
  214. expect(arrow.endBinding.elementId).toBe(clonedRectangle.id);
  215. }
  216. if (arrow.startBinding) {
  217. expect(arrow.startBinding.elementId).toBe(clonedRectangle.id);
  218. }
  219. });
  220. expect(clonedArrow2.boundElements).toEqual([
  221. { type: "text", id: clonedArrowLabel.id },
  222. ]);
  223. expect(clonedArrowLabel.containerId).toBe(clonedArrow2.id);
  224. });
  225. it("should remove id references of elements that aren't found", () => {
  226. const rectangle1 = API.createElement({
  227. type: "rectangle",
  228. id: "rectangle1",
  229. boundElements: [
  230. // should keep
  231. { id: "arrow1", type: "arrow" },
  232. // should drop
  233. { id: "arrow-not-exists", type: "arrow" },
  234. // should drop
  235. { id: "text-not-exists", type: "text" },
  236. ],
  237. });
  238. const arrow1 = API.createElement({
  239. type: "arrow",
  240. id: "arrow1",
  241. startBinding: {
  242. elementId: "rectangle1",
  243. focus: 0.2,
  244. gap: 7,
  245. fixedPoint: [0.5, 1],
  246. },
  247. });
  248. const text1 = API.createElement({
  249. type: "text",
  250. id: "text1",
  251. containerId: "rectangle-not-exists",
  252. });
  253. const arrow2 = API.createElement({
  254. type: "arrow",
  255. id: "arrow2",
  256. startBinding: {
  257. elementId: "rectangle1",
  258. focus: 0.2,
  259. gap: 7,
  260. fixedPoint: [0.5, 1],
  261. },
  262. endBinding: {
  263. elementId: "rectangle-not-exists",
  264. focus: 0.2,
  265. gap: 7,
  266. fixedPoint: [0.5, 1],
  267. },
  268. });
  269. const arrow3 = API.createElement({
  270. type: "arrow",
  271. id: "arrow3",
  272. startBinding: {
  273. elementId: "rectangle-not-exists",
  274. focus: 0.2,
  275. gap: 7,
  276. fixedPoint: [0.5, 1],
  277. },
  278. endBinding: {
  279. elementId: "rectangle1",
  280. focus: 0.2,
  281. gap: 7,
  282. fixedPoint: [0.5, 1],
  283. },
  284. });
  285. // -------------------------------------------------------------------------
  286. const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
  287. const { newElements: clonedElements } = duplicateElements({
  288. type: "everything",
  289. elements: origElements,
  290. }) as any as { newElements: typeof origElements };
  291. const [
  292. clonedRectangle,
  293. clonedText1,
  294. clonedArrow1,
  295. clonedArrow2,
  296. clonedArrow3,
  297. ] = clonedElements;
  298. expect(clonedRectangle.boundElements).toEqual([
  299. { id: clonedArrow1.id, type: "arrow" },
  300. ]);
  301. expect(clonedText1.containerId).toBe(null);
  302. expect(clonedArrow2.startBinding).toEqual({
  303. ...arrow2.startBinding,
  304. elementId: clonedRectangle.id,
  305. });
  306. expect(clonedArrow2.endBinding).toBe(null);
  307. expect(clonedArrow3.startBinding).toBe(null);
  308. expect(clonedArrow3.endBinding).toEqual({
  309. ...arrow3.endBinding,
  310. elementId: clonedRectangle.id,
  311. });
  312. });
  313. describe("should duplicate all group ids", () => {
  314. it("should regenerate all group ids and keep them consistent across elements", () => {
  315. const rectangle1 = API.createElement({
  316. type: "rectangle",
  317. groupIds: ["g1"],
  318. });
  319. const rectangle2 = API.createElement({
  320. type: "rectangle",
  321. groupIds: ["g2", "g1"],
  322. });
  323. const rectangle3 = API.createElement({
  324. type: "rectangle",
  325. groupIds: ["g2", "g1"],
  326. });
  327. const origElements = [rectangle1, rectangle2, rectangle3] as const;
  328. const { newElements: clonedElements } = duplicateElements({
  329. type: "everything",
  330. elements: origElements,
  331. }) as any as { newElements: typeof origElements };
  332. const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
  333. clonedElements;
  334. expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
  335. expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
  336. expect(rectangle2.groupIds[1]).not.toBe(clonedRectangle2.groupIds[1]);
  337. expect(clonedRectangle1.groupIds[0]).toBe(clonedRectangle2.groupIds[1]);
  338. expect(clonedRectangle2.groupIds[0]).toBe(clonedRectangle3.groupIds[0]);
  339. expect(clonedRectangle2.groupIds[1]).toBe(clonedRectangle3.groupIds[1]);
  340. });
  341. it("should keep and regenerate ids of groups even if invalid", () => {
  342. // lone element shouldn't be able to be grouped with itself,
  343. // but hard to check against in a performant way so we ignore it
  344. const rectangle1 = API.createElement({
  345. type: "rectangle",
  346. groupIds: ["g1"],
  347. });
  348. const {
  349. newElements: [clonedRectangle1],
  350. } = duplicateElements({ type: "everything", elements: [rectangle1] });
  351. expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
  352. expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
  353. });
  354. });
  355. });
  356. describe("duplication z-order", () => {
  357. beforeEach(async () => {
  358. await render(<Excalidraw />);
  359. });
  360. it("duplication z order with Cmd+D for the lowest z-ordered element should be +1 for the clone", () => {
  361. const rectangle1 = API.createElement({
  362. type: "rectangle",
  363. x: 0,
  364. y: 0,
  365. });
  366. const rectangle2 = API.createElement({
  367. type: "rectangle",
  368. x: 10,
  369. y: 10,
  370. });
  371. const rectangle3 = API.createElement({
  372. type: "rectangle",
  373. x: 20,
  374. y: 20,
  375. });
  376. API.setElements([rectangle1, rectangle2, rectangle3]);
  377. API.setSelectedElements([rectangle1]);
  378. act(() => {
  379. h.app.actionManager.executeAction(actionDuplicateSelection);
  380. });
  381. assertElements(h.elements, [
  382. { id: rectangle1.id },
  383. { [ORIG_ID]: rectangle1.id, selected: true },
  384. { id: rectangle2.id },
  385. { id: rectangle3.id },
  386. ]);
  387. });
  388. it("duplication z order with Cmd+D for the highest z-ordered element should be +1 for the clone", () => {
  389. const rectangle1 = API.createElement({
  390. type: "rectangle",
  391. x: 0,
  392. y: 0,
  393. });
  394. const rectangle2 = API.createElement({
  395. type: "rectangle",
  396. x: 10,
  397. y: 10,
  398. });
  399. const rectangle3 = API.createElement({
  400. type: "rectangle",
  401. x: 20,
  402. y: 20,
  403. });
  404. API.setElements([rectangle1, rectangle2, rectangle3]);
  405. API.setSelectedElements([rectangle3]);
  406. act(() => {
  407. h.app.actionManager.executeAction(actionDuplicateSelection);
  408. });
  409. assertElements(h.elements, [
  410. { id: rectangle1.id },
  411. { id: rectangle2.id },
  412. { id: rectangle3.id },
  413. { [ORIG_ID]: rectangle3.id, selected: true },
  414. ]);
  415. });
  416. it("duplication z order with alt+drag for the lowest z-ordered element should be +1 for the clone", () => {
  417. const rectangle1 = API.createElement({
  418. type: "rectangle",
  419. x: 0,
  420. y: 0,
  421. });
  422. const rectangle2 = API.createElement({
  423. type: "rectangle",
  424. x: 10,
  425. y: 10,
  426. });
  427. const rectangle3 = API.createElement({
  428. type: "rectangle",
  429. x: 20,
  430. y: 20,
  431. });
  432. API.setElements([rectangle1, rectangle2, rectangle3]);
  433. mouse.select(rectangle1);
  434. Keyboard.withModifierKeys({ alt: true }, () => {
  435. mouse.down(rectangle1.x + 5, rectangle1.y + 5);
  436. mouse.up(rectangle1.x + 5, rectangle1.y + 5);
  437. });
  438. assertElements(h.elements, [
  439. { [ORIG_ID]: rectangle1.id },
  440. { id: rectangle1.id, selected: true },
  441. { id: rectangle2.id },
  442. { id: rectangle3.id },
  443. ]);
  444. });
  445. it("duplication z order with alt+drag for the highest z-ordered element should be +1 for the clone", () => {
  446. const rectangle1 = API.createElement({
  447. type: "rectangle",
  448. x: 0,
  449. y: 0,
  450. });
  451. const rectangle2 = API.createElement({
  452. type: "rectangle",
  453. x: 10,
  454. y: 10,
  455. });
  456. const rectangle3 = API.createElement({
  457. type: "rectangle",
  458. x: 20,
  459. y: 20,
  460. });
  461. API.setElements([rectangle1, rectangle2, rectangle3]);
  462. mouse.select(rectangle3);
  463. Keyboard.withModifierKeys({ alt: true }, () => {
  464. mouse.down(rectangle3.x + 5, rectangle3.y + 5);
  465. mouse.up(rectangle3.x + 5, rectangle3.y + 5);
  466. });
  467. assertElements(h.elements, [
  468. { id: rectangle1.id },
  469. { id: rectangle2.id },
  470. { [ORIG_ID]: rectangle3.id },
  471. { id: rectangle3.id, selected: true },
  472. ]);
  473. });
  474. it("duplication z order with alt+drag for the lowest z-ordered element should be +1 for the clone", () => {
  475. const rectangle1 = API.createElement({
  476. type: "rectangle",
  477. x: 0,
  478. y: 0,
  479. });
  480. const rectangle2 = API.createElement({
  481. type: "rectangle",
  482. x: 10,
  483. y: 10,
  484. });
  485. const rectangle3 = API.createElement({
  486. type: "rectangle",
  487. x: 20,
  488. y: 20,
  489. });
  490. API.setElements([rectangle1, rectangle2, rectangle3]);
  491. mouse.select(rectangle1);
  492. Keyboard.withModifierKeys({ alt: true }, () => {
  493. mouse.down(rectangle1.x + 5, rectangle1.y + 5);
  494. mouse.up(rectangle1.x + 5, rectangle1.y + 5);
  495. });
  496. assertElements(h.elements, [
  497. { [ORIG_ID]: rectangle1.id },
  498. { id: rectangle1.id, selected: true },
  499. { id: rectangle2.id },
  500. { id: rectangle3.id },
  501. ]);
  502. });
  503. it("duplication z order with alt+drag with grouped elements should consider the group together when determining z-index", () => {
  504. const rectangle1 = API.createElement({
  505. type: "rectangle",
  506. x: 0,
  507. y: 0,
  508. groupIds: ["group1"],
  509. });
  510. const rectangle2 = API.createElement({
  511. type: "rectangle",
  512. x: 10,
  513. y: 10,
  514. groupIds: ["group1"],
  515. });
  516. const rectangle3 = API.createElement({
  517. type: "rectangle",
  518. x: 20,
  519. y: 20,
  520. groupIds: ["group1"],
  521. });
  522. API.setElements([rectangle1, rectangle2, rectangle3]);
  523. mouse.select(rectangle1);
  524. Keyboard.withModifierKeys({ alt: true }, () => {
  525. mouse.down(rectangle1.x + 5, rectangle1.y + 5);
  526. mouse.up(rectangle1.x + 15, rectangle1.y + 15);
  527. });
  528. assertElements(h.elements, [
  529. { [ORIG_ID]: rectangle1.id },
  530. { [ORIG_ID]: rectangle2.id },
  531. { [ORIG_ID]: rectangle3.id },
  532. { id: rectangle1.id, selected: true },
  533. { id: rectangle2.id, selected: true },
  534. { id: rectangle3.id, selected: true },
  535. ]);
  536. });
  537. it("reverse-duplicating text container (in-order)", async () => {
  538. const [rectangle, text] = API.createTextContainer();
  539. API.setElements([rectangle, text]);
  540. API.setSelectedElements([rectangle, text]);
  541. Keyboard.withModifierKeys({ alt: true }, () => {
  542. mouse.down(rectangle.x + 5, rectangle.y + 5);
  543. mouse.up(rectangle.x + 15, rectangle.y + 15);
  544. });
  545. assertElements(h.elements, [
  546. { [ORIG_ID]: rectangle.id },
  547. {
  548. [ORIG_ID]: text.id,
  549. containerId: getCloneByOrigId(rectangle.id)?.id,
  550. },
  551. { id: rectangle.id, selected: true },
  552. { id: text.id, containerId: rectangle.id, selected: true },
  553. ]);
  554. });
  555. it("reverse-duplicating text container (out-of-order)", async () => {
  556. const [rectangle, text] = API.createTextContainer();
  557. API.setElements([text, rectangle]);
  558. API.setSelectedElements([rectangle, text]);
  559. Keyboard.withModifierKeys({ alt: true }, () => {
  560. mouse.down(rectangle.x + 5, rectangle.y + 5);
  561. mouse.up(rectangle.x + 15, rectangle.y + 15);
  562. });
  563. assertElements(h.elements, [
  564. { [ORIG_ID]: rectangle.id },
  565. {
  566. [ORIG_ID]: text.id,
  567. containerId: getCloneByOrigId(rectangle.id)?.id,
  568. },
  569. { id: rectangle.id, selected: true },
  570. { id: text.id, containerId: rectangle.id, selected: true },
  571. ]);
  572. });
  573. it("reverse-duplicating labeled arrows (in-order)", async () => {
  574. const [arrow, text] = API.createLabeledArrow();
  575. API.setElements([arrow, text]);
  576. API.setSelectedElements([arrow, text]);
  577. Keyboard.withModifierKeys({ alt: true }, () => {
  578. mouse.down(arrow.x + 5, arrow.y + 5);
  579. mouse.up(arrow.x + 15, arrow.y + 15);
  580. });
  581. assertElements(h.elements, [
  582. { [ORIG_ID]: arrow.id },
  583. {
  584. [ORIG_ID]: text.id,
  585. containerId: getCloneByOrigId(arrow.id)?.id,
  586. },
  587. { id: arrow.id, selected: true },
  588. { id: text.id, containerId: arrow.id, selected: true },
  589. ]);
  590. });
  591. it("reverse-duplicating labeled arrows (out-of-order)", async () => {
  592. const [arrow, text] = API.createLabeledArrow();
  593. API.setElements([text, arrow]);
  594. API.setSelectedElements([arrow, text]);
  595. Keyboard.withModifierKeys({ alt: true }, () => {
  596. mouse.down(arrow.x + 5, arrow.y + 5);
  597. mouse.up(arrow.x + 15, arrow.y + 15);
  598. });
  599. assertElements(h.elements, [
  600. { [ORIG_ID]: arrow.id },
  601. {
  602. [ORIG_ID]: text.id,
  603. containerId: getCloneByOrigId(arrow.id)?.id,
  604. },
  605. { id: arrow.id, selected: true },
  606. { id: text.id, containerId: arrow.id, selected: true },
  607. ]);
  608. });
  609. });