diff --git a/e2e/canvas/move-shape-using-keyboard.spec.ts b/e2e/canvas/move-shape-using-keyboard.spec.ts new file mode 100644 index 00000000..196917c6 --- /dev/null +++ b/e2e/canvas/move-shape-using-keyboard.spec.ts @@ -0,0 +1,111 @@ +import { test, expect } from '@playwright/test'; +import { + dragAndDrop, + getLocatorPosition, + getByShapeType, + getShapePosition, + moveSelected, +} from '../helpers'; +import { Group } from 'konva/lib/Group'; + +const numShifts: number = 10; + +test('move shape with the keyboard - left', async ({ page }) => { + await page.goto(''); + + const component = page.getByAltText('Input', { exact: true }); + const position = await getLocatorPosition(component); + + await dragAndDrop(page, position, { + x: position.x + 500, + y: position.y - 240, + }); + + const inputShape = (await getByShapeType(page, 'input')) as Group; + expect(inputShape).toBeDefined(); + + const draggedPosition = await getShapePosition(inputShape); + + moveSelected(page, 'ArrowLeft', numShifts); + + const inputShapeMoved = (await getByShapeType(page, 'input')) as Group; + const movedPosition = await getShapePosition(inputShapeMoved); + + expect(movedPosition.x === draggedPosition.x - numShifts * 2).toBeTruthy(); + expect(movedPosition.y === draggedPosition.y).toBeTruthy(); +}); + +test('move shape with the keyboard - up', async ({ page }) => { + await page.goto(''); + + const component = page.getByAltText('Input', { exact: true }); + const position = await getLocatorPosition(component); + + await dragAndDrop(page, position, { + x: position.x + 500, + y: position.y - 240, + }); + + const inputShape = (await getByShapeType(page, 'input')) as Group; + expect(inputShape).toBeDefined(); + + const draggedPosition = await getShapePosition(inputShape); + + moveSelected(page, 'ArrowUp', numShifts); + + const inputShapeMoved = (await getByShapeType(page, 'input')) as Group; + const movedPosition = await getShapePosition(inputShapeMoved); + + expect(movedPosition.x === draggedPosition.x).toBeTruthy(); + expect(movedPosition.y === draggedPosition.y - numShifts * 2).toBeTruthy(); +}); + +test('move shape with the keyboard - down', async ({ page }) => { + await page.goto(''); + + const component = page.getByAltText('Input', { exact: true }); + const position = await getLocatorPosition(component); + + await dragAndDrop(page, position, { + x: position.x + 500, + y: position.y - 240, + }); + + const inputShape = (await getByShapeType(page, 'input')) as Group; + expect(inputShape).toBeDefined(); + + const draggedPosition = await getShapePosition(inputShape); + + moveSelected(page, 'ArrowDown', numShifts); + + const inputShapeMoved = (await getByShapeType(page, 'input')) as Group; + const movedPosition = await getShapePosition(inputShapeMoved); + + expect(movedPosition.x === draggedPosition.x).toBeTruthy(); + expect(movedPosition.y === draggedPosition.y + numShifts * 2).toBeTruthy(); +}); + +test('move shape with the keyboard - right', async ({ page }) => { + await page.goto(''); + + const component = page.getByAltText('Input', { exact: true }); + const position = await getLocatorPosition(component); + + await dragAndDrop(page, position, { + x: position.x + 500, + y: position.y - 240, + }); + + const inputShape = (await getByShapeType(page, 'input')) as Group; + expect(inputShape).toBeDefined(); + + const draggedPosition = await getShapePosition(inputShape); + + moveSelected(page, 'ArrowRight', numShifts); + + const inputShapeMoved = (await getByShapeType(page, 'input')) as Group; + const movedPosition = await getShapePosition(inputShapeMoved); + + expect(movedPosition.x === draggedPosition.x + numShifts * 2).toBeTruthy(); + expect(movedPosition.y === draggedPosition.y).toBeTruthy(); +}); diff --git a/e2e/helpers/konva-testing.helpers.ts b/e2e/helpers/konva-testing.helpers.ts index 22f2fe21..fc747d50 100644 --- a/e2e/helpers/konva-testing.helpers.ts +++ b/e2e/helpers/konva-testing.helpers.ts @@ -2,6 +2,8 @@ import { Page } from '@playwright/test'; import { Layer } from 'konva/lib/Layer'; import { Shape } from 'konva/lib/Shape'; import { Group } from 'konva/lib/Group'; +import { E2E_CanvasItemKeyAttrs } from './types/e2e-types'; +import { getCanvasBoundingBox } from './position.helpers'; const getLayer = async (page: Page): Promise => await page.evaluate(() => { @@ -59,3 +61,60 @@ export const getTransformer = async (page: Page): Promise => { }); return transformer; }; + +export const getWithinCanvasItemList = async ( + page: Page +): Promise => { + const items = await page.evaluate(() => { + return window.__TESTING_KONVA_LAYER__.find( + (c: any) => c.getType('Group') && (c.attrs['data-id'] as Group) + ); + }); + return items.map(it => it.attrs); +}; + +export const clickOnCanvasItem = async ( + page: Page, + item: E2E_CanvasItemKeyAttrs +) => { + const { x, y } = item; + const stageCanvas = await page.locator('#konva-stage canvas').first(); + const canvasWindowPos = await stageCanvas.boundingBox(); + if (!canvasWindowPos) throw new Error('Canvas is not loaded on ui'); + await page.mouse.move( + canvasWindowPos?.x + x + 20, + canvasWindowPos?.y + y + 20 + ); + + await page.mouse.down(); + await page.mouse.up(); + + return item; +}; + +export const dbClickOnCanvasItem = async ( + page: Page, + item: E2E_CanvasItemKeyAttrs +) => { + const { x, y } = item; + const canvasWindowPos = await getCanvasBoundingBox(page); + await page.mouse.dblclick( + canvasWindowPos?.x + x + 20, + canvasWindowPos?.y + y + 20 + ); + return item; +}; + +export const ctrlClickOverCanvasItems = async ( + page: Page, + itemList: E2E_CanvasItemKeyAttrs[] +) => { + if (!itemList.length) + throw new Error('Please, add an array with at least one canvas Item'); + // NOTE: The keyboard entry 'ControlOrMeta' is the way to simulate both 'Ctrl' or 'Command' key + await page.keyboard.down('ControlOrMeta'); + for (const item of itemList) { + await clickOnCanvasItem(page, item); + } + await page.keyboard.up('ControlOrMeta'); +}; diff --git a/e2e/helpers/position.helpers.ts b/e2e/helpers/position.helpers.ts index 84485272..c29f19f1 100644 --- a/e2e/helpers/position.helpers.ts +++ b/e2e/helpers/position.helpers.ts @@ -1,4 +1,5 @@ import { Locator, Page } from '@playwright/test'; +import { Group } from 'konva/lib/Group'; interface Position { x: number; @@ -8,6 +9,7 @@ interface Position { export const getLocatorPosition = async ( locator: Locator ): Promise => { + await locator.scrollIntoViewIfNeeded(); const box = (await locator.boundingBox()) || { x: 0, y: 0, @@ -17,6 +19,17 @@ export const getLocatorPosition = async ( return { x: box.x + box.width / 2, y: box.y + box.height / 2 }; }; +export const getCanvasBoundingBox = async (page: Page) => { + const canvasWindowPos = await page + .locator('#konva-stage canvas') + .boundingBox(); + if (canvasWindowPos) { + return canvasWindowPos; + } else { + throw new Error('Canvas is not loaded on ui'); + } +}; + export const dragAndDrop = async ( page: Page, aPosition: Position, @@ -32,11 +45,13 @@ export const addComponentsToCanvas = async ( page: Page, components: string[] ) => { - const canvasPosition = await page.locator('canvas').boundingBox(); + const stageCanvas = await page.locator('#konva-stage canvas').first(); + const canvasPosition = await stageCanvas.boundingBox(); if (!canvasPosition) throw new Error('No canvas found'); for await (const [index, c] of components.entries()) { const component = page.getByAltText(c, { exact: true }); + await component.scrollIntoViewIfNeeded(); const position = await getLocatorPosition(component); const targetPosition = ( @@ -53,3 +68,15 @@ export const addComponentsToCanvas = async ( await dragAndDrop(page, position, targetPosition(120, index)); } }; + +export const getShapePosition = async (shape: Group): Promise => { + return { x: shape?.attrs.x, y: shape?.attrs.y }; +}; + +export const moveSelected = ( + page: Page, + direction: string, + numShifts: number +) => { + for (let i: number = 0; i < numShifts; i++) page.keyboard.down(direction); +}; diff --git a/e2e/helpers/types/e2e-types.d.ts b/e2e/helpers/types/e2e-types.d.ts new file mode 100644 index 00000000..d755d3f1 --- /dev/null +++ b/e2e/helpers/types/e2e-types.d.ts @@ -0,0 +1,8 @@ +export interface E2E_CanvasItemKeyAttrs { + x: number; + y: number; + ['data-id']: string; + width: number; + height: number; + shapeType: string; +} diff --git a/e2e/helpers/ui-buttons.helpers.ts b/e2e/helpers/ui-buttons.helpers.ts new file mode 100644 index 00000000..9dad6572 --- /dev/null +++ b/e2e/helpers/ui-buttons.helpers.ts @@ -0,0 +1,13 @@ +import { Page } from '@playwright/test'; + +export const clickUndoUiButton = async (page: Page) => + await page.getByRole('button', { name: 'Undo' }).click(); + +export const clickRedoUiButton = async (page: Page) => + await page.getByRole('button', { name: 'Redo' }).click(); + +export const clickCopyUiButton = async (page: Page) => + await page.getByRole('button', { name: 'Copy' }).click(); + +export const clickPasteUiButton = async (page: Page) => + await page.getByRole('button', { name: 'Paste' }).click(); diff --git a/e2e/inline-edit/multiple-line-inline-edit.spec.ts b/e2e/inline-edit/multiple-line-inline-edit.spec.ts new file mode 100644 index 00000000..ad85802c --- /dev/null +++ b/e2e/inline-edit/multiple-line-inline-edit.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from '@playwright/test'; +import { Group } from 'konva/lib/Group'; +import { dragAndDrop, getByShapeType, getLocatorPosition } from '../helpers'; + +test('can add textarea to canvas, edit content, and verify shape text', async ({ + page, +}) => { + await page.goto(''); + const component = page.getByAltText('Textarea'); + await component.scrollIntoViewIfNeeded(); + + const position = await getLocatorPosition(component); + const targetPosition = { + x: position.x + 500, + y: position.y - 240, + }; + await dragAndDrop(page, position, targetPosition); + await page.mouse.dblclick(targetPosition.x, targetPosition.y + 40); + const textarea = page.getByRole('textbox').first(); + const textareaContent = await textarea.inputValue(); + expect(textareaContent).toEqual('Your text here...'); + + const textContent = 'Hello'; + await textarea.fill(textContent); + await page.mouse.click(800, 130); + const textareaShape = (await getByShapeType(page, 'textarea')) as Group; + + expect(textareaShape).toBeDefined(); + const textShape = textareaShape.children.find( + child => child.attrs.text === textContent + ); + expect(textShape).toBeDefined(); +}); + +test('cancels textarea edit on Escape and verifies original shape text', async ({ + page, +}) => { + await page.goto(''); + const component = page.getByAltText('Textarea'); + await component.scrollIntoViewIfNeeded(); + + const position = await getLocatorPosition(component); + const targetPosition = { + x: position.x + 500, + y: position.y - 240, + }; + await dragAndDrop(page, position, targetPosition); + await page.mouse.dblclick(targetPosition.x, targetPosition.y + 40); + const textarea = page.getByRole('textbox').first(); + + const textContent = 'Hello'; + await textarea.fill(textContent); + await page.keyboard.press('Escape'); + const originalTextContent = 'Your text here...'; + const textareaShape = (await getByShapeType(page, 'textarea')) as Group; + + expect(textareaShape).toBeDefined(); + const textShape = textareaShape.children.find( + child => child.attrs.text === originalTextContent + ); + expect(textShape).toBeDefined(); +}); + +test('can add and edit input, and delete last letter', async ({ page }) => { + await page.goto(''); + const component = page.getByAltText('Textarea'); + await component.scrollIntoViewIfNeeded(); + + const position = await getLocatorPosition(component); + const targetPosition = { + x: position.x + 500, + y: position.y - 240, + }; + await dragAndDrop(page, position, targetPosition); + await page.mouse.dblclick(targetPosition.x, targetPosition.y + 40); + const textarea = page.getByRole('textbox').first(); + + const textContent = 'World'; + await textarea.fill(textContent); + await page.keyboard.press('Backspace'); + const updatedTextareaContent = await textarea.inputValue(); + expect(updatedTextareaContent).toEqual('Worl'); + + await page.mouse.click(800, 130); + + const textareaShape = (await getByShapeType(page, 'textarea')) as Group; + expect(textareaShape).toBeDefined(); + const textShape = textareaShape.children.find( + child => child.attrs.text === 'Worl' + ); + expect(textShape).toBeDefined(); +}); + +test('adds multi-line text to textarea on canvas and verifies shape text', async ({ + page, +}) => { + await page.goto(''); + const component = page.getByAltText('Textarea'); + await component.scrollIntoViewIfNeeded(); + + const position = await getLocatorPosition(component); + const targetPosition = { + x: position.x + 500, + y: position.y - 240, + }; + await dragAndDrop(page, position, targetPosition); + await page.mouse.dblclick(targetPosition.x, targetPosition.y + 40); + const textarea = page.getByRole('textbox').first(); + + const textContent = 'Line 1\nLine 2'; + await textarea.fill(textContent); + + await page.mouse.click(800, 130); + + const textareaShape = (await getByShapeType(page, 'textarea')) as Group; + expect(textareaShape).toBeDefined(); + const textShape = textareaShape.children.find( + child => child.attrs.text === textContent + ); + expect(textShape).toBeDefined(); +}); diff --git a/e2e/inline-edit/simple-inline-edit.spec.ts b/e2e/inline-edit/simple-inline-edit.spec.ts index b29d1a14..7057a1b7 100644 --- a/e2e/inline-edit/simple-inline-edit.spec.ts +++ b/e2e/inline-edit/simple-inline-edit.spec.ts @@ -61,3 +61,29 @@ test('can add and edit input, and delete last letter', async ({ page }) => { ); expect(textShape).toBeDefined(); }); + +test('can edit input, press Esc key, and cancel edition', async ({ page }) => { + await page.goto(''); + const component = page.getByAltText('Input', { exact: true }); + + const position = await getLocatorPosition(component); + const targetPosition = { + x: position.x + 500, + y: position.y - 240, + }; + await dragAndDrop(page, position, targetPosition); + await page.mouse.dblclick(targetPosition.x, targetPosition.y); + const input = page.getByRole('textbox').first(); + const inputValue = await input.getAttribute('value'); + expect(inputValue).toEqual('Placeholder'); + + const textContent = 'User'; + await input.fill(textContent); + await page.keyboard.press('Escape'); + const inputShape = (await getByShapeType(page, 'input')) as Group; + expect(inputShape).toBeDefined(); + const textShape = inputShape.children.find( + child => child.attrs.text === 'Placeholder' + ); + expect(textShape).toBeDefined(); +}); diff --git a/e2e/selection/multiple-selection.spec.ts b/e2e/selection/multiple-selection.spec.ts index 5992b0ee..7866ce20 100644 --- a/e2e/selection/multiple-selection.spec.ts +++ b/e2e/selection/multiple-selection.spec.ts @@ -1,5 +1,11 @@ import { test, expect } from '@playwright/test'; -import { dragAndDrop, addComponentsToCanvas, getTransformer } from '../helpers'; +import { + dragAndDrop, + addComponentsToCanvas, + getTransformer, + getWithinCanvasItemList, + ctrlClickOverCanvasItems, +} from '../helpers'; test('Should perform multiple selection when dragging and dropping over multiple components in the canvas', async ({ page, @@ -20,3 +26,52 @@ test('Should perform multiple selection when dragging and dropping over multiple const selectedItems = await getTransformer(page); expect(selectedItems._nodes.length).toEqual(3); }); + +test('Should deselect all previously selected items when clicking on an empty point on the canvas', async ({ + page, +}) => { + await page.goto(''); + + //Drag and drop component to canvas + const componentsAtCanvas = ['Input', 'Input', 'Icon', 'Label']; + await addComponentsToCanvas(page, componentsAtCanvas); + + //Click Away + await page.mouse.click(800, 130); + + //Perform items selection by drag and drop + await dragAndDrop(page, { x: 260, y: 130 }, { x: 1000, y: 550 }); + + //Assert + const selectedItems = await getTransformer(page); + expect(selectedItems._nodes.length).toEqual(3); + + //Click Away + await page.mouse.click(800, 130); + //Assert + const updatedSelectedItems = await getTransformer(page); + expect(updatedSelectedItems._nodes.length).toEqual(0); +}); + +test('Should add some in-canvas-items to the current selection, by clicking on them, while pressing the CTRL / CMD keyboard.', async ({ + page, +}) => { + await page.goto(''); + + //Drag and drop component to canvas + const componentsAtCanvas = ['Input', 'Button', 'Textarea', 'Combobox']; + await addComponentsToCanvas(page, componentsAtCanvas); + const insideCanvasItemList = await getWithinCanvasItemList(page); + + //Assert no elements at current selection + const selectedItems = await getTransformer(page); + expect(selectedItems._nodes.length).toEqual(1); + + // Add 2 canvas items to current selection + const itemsToBeSelected = insideCanvasItemList.slice(1, 3); + await ctrlClickOverCanvasItems(page, itemsToBeSelected); + + //Assert the quantity of selected-items + const currentSelection = await getTransformer(page); + expect(currentSelection._nodes.length).toEqual(3); +}); diff --git a/e2e/selection/shape-selection.spec.ts b/e2e/selection/shape-selection.spec.ts index ad9a7b93..cc352ccc 100644 --- a/e2e/selection/shape-selection.spec.ts +++ b/e2e/selection/shape-selection.spec.ts @@ -46,7 +46,8 @@ test('drop shape in canvas, click on canvas, drop diselected', async ({ const inputShape = (await getByShapeType(page, 'input')) as Group; expect(inputShape).toBeDefined(); - await page.click('canvas'); + //Click Away + await page.mouse.click(800, 130); const transformer = await getTransformer(page); expect(transformer).toBeDefined(); diff --git a/e2e/ui-functionality/copy-paste.spec.ts b/e2e/ui-functionality/copy-paste.spec.ts new file mode 100644 index 00000000..3b085f34 --- /dev/null +++ b/e2e/ui-functionality/copy-paste.spec.ts @@ -0,0 +1,158 @@ +import { test, expect } from '@playwright/test'; +import { + addComponentsToCanvas, + dragAndDrop, + getWithinCanvasItemList, +} from '../helpers'; +import { E2E_CanvasItemKeyAttrs } from '../helpers/types/e2e-types'; +import { + clickCopyUiButton, + clickPasteUiButton, +} from '../helpers/ui-buttons.helpers'; + +test.describe('Copy/Paste functionality tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(''); + }); + + test('Should copy and paste a single shape using the ToolBar UI buttons', async ({ + page, + }) => { + //Arrange one Input component + const addInputIntoCanvas = ['Input']; + await addComponentsToCanvas(page, addInputIntoCanvas); + + //Copy and assert there are only one component within the canvas + await clickCopyUiButton(page); + const insideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + expect(insideCanvasItemList.length).toEqual(1); + + //Paste and assert there are 2 Input Components and that they have different IDs + await clickPasteUiButton(page); + const updatedInsideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [originalComponent, copiedComponent] = updatedInsideCanvasItemList; + + expect(updatedInsideCanvasItemList.length).toEqual(2); + expect(originalComponent.shapeType).toEqual(copiedComponent.shapeType); + expect(originalComponent['data-id']).not.toEqual( + copiedComponent['data-id'] + ); + }); + + test('Should copy and paste a single shape using keyboard commands', async ({ + page, + }) => { + // NOTE: This test has the same steps as the previous one, except for the keyboard commands. + //Arrange one Input component + const addInputIntoCanvas = ['Input']; + await addComponentsToCanvas(page, addInputIntoCanvas); + + //Copy and assert there are only one component within the canvas + await page.keyboard.press('ControlOrMeta+c'); + const insideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + expect(insideCanvasItemList.length).toEqual(1); + + //Paste and assert there are 2 Input Components and that they have different IDs + await page.keyboard.press('ControlOrMeta+v'); + const updatedInsideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [originalComponent, copiedComponent] = updatedInsideCanvasItemList; + + expect(updatedInsideCanvasItemList.length).toEqual(2); + expect(originalComponent.shapeType).toEqual(copiedComponent.shapeType); + expect(originalComponent['data-id']).not.toEqual( + copiedComponent['data-id'] + ); + }); + + /* + test('Should copy and paste a multiple shapes using the ToolBar UI buttons', async ({ + page, + }) => { + //Add several components into the canvas + const addInputIntoCanvas = ['Input', 'Combobox', 'Icon']; + await addComponentsToCanvas(page, addInputIntoCanvas); + + //Select items by drag and drop + await dragAndDrop(page, { x: 260, y: 130 }, { x: 1000, y: 550 }); + + //Copy and assert there are 3 components within the canvas + await clickCopyUiButton(page); + const insideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [originalComp_1, originalComp_2, originalComp_3] = + insideCanvasItemList; + expect(insideCanvasItemList.length).toEqual(3); + + //Paste + await clickPasteUiButton(page); + const updatedInsideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [, , , copiedComp_1, copiedComp_2, copiedComp_3] = + updatedInsideCanvasItemList; + + //Assert there are 6 Components, + expect(updatedInsideCanvasItemList.length).toEqual(6); + + //Assert they match the same shapes respectively + expect(originalComp_1.shapeType).toEqual(copiedComp_1.shapeType); + expect(originalComp_2.shapeType).toEqual(copiedComp_2.shapeType); + expect(originalComp_3.shapeType).toEqual(copiedComp_3.shapeType); + + //Assert they have different IDs + expect(originalComp_1['data-id']).not.toEqual(copiedComp_1['data-id']); + expect(originalComp_2['data-id']).not.toEqual(copiedComp_2['data-id']); + expect(originalComp_3['data-id']).not.toEqual(copiedComp_3['data-id']); + }); +*/ + test('Should copy and paste a multiple shapes using keyboard commands', async ({ + page, + }) => { + // NOTE: This test has the same steps as the previous one, except for the keyboard commands. + //Add several components into the canvas + const addInputIntoCanvas = ['Input', 'Combobox', 'Icon']; + await addComponentsToCanvas(page, addInputIntoCanvas); + + //Select items by drag and drop + await dragAndDrop(page, { x: 260, y: 130 }, { x: 1000, y: 550 }); + + //Copy and assert there are 3 components within the canvas + await page.keyboard.press('ControlOrMeta+c'); + const insideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [originalComp_1, originalComp_2, originalComp_3] = + insideCanvasItemList; + expect(insideCanvasItemList.length).toEqual(3); + + //Paste + await page.keyboard.press('ControlOrMeta+v'); + const updatedInsideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [, , , copiedComp_1, copiedComp_2, copiedComp_3] = + updatedInsideCanvasItemList; + + //Assert there are 6 Components, + expect(updatedInsideCanvasItemList.length).toEqual(6); + + //Assert they match the same shapes respectively + expect(originalComp_1.shapeType).toEqual(copiedComp_1.shapeType); + expect(originalComp_2.shapeType).toEqual(copiedComp_2.shapeType); + expect(originalComp_3.shapeType).toEqual(copiedComp_3.shapeType); + + //Assert they have different IDs + expect(originalComp_1['data-id']).not.toEqual(copiedComp_1['data-id']); + expect(originalComp_2['data-id']).not.toEqual(copiedComp_2['data-id']); + expect(originalComp_3['data-id']).not.toEqual(copiedComp_3['data-id']); + }); +}); diff --git a/e2e/ui-functionality/toolbar_undo-redo.spec.ts b/e2e/ui-functionality/toolbar_undo-redo.spec.ts new file mode 100644 index 00000000..c9ad1c68 --- /dev/null +++ b/e2e/ui-functionality/toolbar_undo-redo.spec.ts @@ -0,0 +1,166 @@ +import { test, expect } from '@playwright/test'; +import { + addComponentsToCanvas, + getWithinCanvasItemList, + getByShapeType, + dbClickOnCanvasItem, + getCanvasBoundingBox, + getShapePosition, +} from '../helpers'; +import { E2E_CanvasItemKeyAttrs } from '../helpers/types/e2e-types'; +import { Group } from 'konva/lib/Group'; +import { + clickRedoUiButton, + clickUndoUiButton, +} from '../helpers/ui-buttons.helpers'; + +test.describe('ToolBar buttons Undo/Redo functionality tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(''); + }); + + test('Should remove and restore a just dragged into canvas-item, respectively', async ({ + page, + }) => { + //Arrange + const addInputIntoCanvas = ['Input']; + await addComponentsToCanvas(page, addInputIntoCanvas); + + //Undo and check within canvas items + await clickUndoUiButton(page); + const insideCanvasItemList = await getWithinCanvasItemList(page); + + expect(insideCanvasItemList.length).toEqual(0); + + //Redo and check existing item within canvas + await clickRedoUiButton(page); + const updatedInsideCanvasItemList = await getWithinCanvasItemList(page); + + expect(updatedInsideCanvasItemList.length).toEqual(1); + }); + + test('Should remove and restore the last item you just dragged into the canvas', async ({ + page, + }) => { + //Arrange + const addComponentsIntoCanvas = ['Input', 'Combobox']; + await addComponentsToCanvas(page, addComponentsIntoCanvas); + + //Undo and assert there is only one Item within canvas + await clickUndoUiButton(page); + const insideCanvasItemList = await getWithinCanvasItemList(page); + + expect(insideCanvasItemList.length).toEqual(1); + + const firsCanvastItem = await getByShapeType(page, 'input'); + expect(firsCanvastItem).toBeDefined(); + + //Redo and assert both items are contained within the canvas + await clickRedoUiButton(page); + const updatedInsideCanvasItemList = await getWithinCanvasItemList(page); + const secondCanvasItem = await getByShapeType(page, 'combobox'); + + expect(updatedInsideCanvasItemList.length).toEqual(2); + expect(firsCanvastItem).toBeDefined(); + expect(secondCanvasItem).toBeDefined(); + }); + + test('Should reverse and restore an edited text of an Input Component', async ({ + page, + }) => { + //Arrange data and drag an input + const addComponentsIntoCanvas = ['Input']; + const defaultInputPlaceholder = 'Placeholder'; + const updatedText = 'Hello'; + + await addComponentsToCanvas(page, addComponentsIntoCanvas); + const [inputOnCanvas] = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + + //Start Input component inline editing + await dbClickOnCanvasItem(page, inputOnCanvas); + const editableInput = page.locator('input[data-is-inline-edition-on=true]'); + const defaultInputValue = await editableInput.inputValue(); + + await editableInput.fill(updatedText); + const updatedInputValue = await editableInput.inputValue(); + + //Undo edit and assert text is reversed + await clickUndoUiButton(page); + expect(defaultInputValue).toEqual(defaultInputPlaceholder); + + //Redo edit and assert that input contains the restored updated text + await clickRedoUiButton(page); + expect(updatedInputValue).toEqual(updatedText); + }); + + test('Should restore the item position to its previous placement', async ({ + page, + }) => { + //Arrange data and drag an input into canvas + const componentToAddintoCanvas = ['Input']; + await addComponentsToCanvas(page, componentToAddintoCanvas); + + const { x: canvasXStart, y: canvasYStart } = + await getCanvasBoundingBox(page); + + const inputElement = (await getByShapeType(page, 'input')) as Group; + + const inputInitialPosition = await getShapePosition(inputElement); + const inputModifiedPosition = { + x: inputInitialPosition.x + canvasXStart + 200, + y: inputInitialPosition.y + canvasYStart, + }; + + //Displace item within the canvas + await page.mouse.down(); + await page.mouse.move(inputModifiedPosition.x, inputModifiedPosition.y); + await page.mouse.up(); + + //Undo and assert that the item is placed in its initial position + await clickUndoUiButton(page); + const finalInputPosition = await getShapePosition(inputElement); + + expect(finalInputPosition).toEqual(inputInitialPosition); + }); + + test('Should undo and redo, backward and forward severals steps consistently', async ({ + page, + }) => { + //Arrange data and drag an items into canvas + const componentsToAddIntoCanvas = ['Input', 'Combobox']; + await addComponentsToCanvas(page, componentsToAddIntoCanvas); + + await page.getByText('Rich Components').click(); + const richComponentsToAddintoCanvas = ['Accordion']; + await addComponentsToCanvas(page, richComponentsToAddintoCanvas); + + //Assert there are 3 items within the canvas + const itemsQtyWithinCanvas_step1 = (await getWithinCanvasItemList(page)) + .length; + + expect(itemsQtyWithinCanvas_step1).toEqual(3); + + //x3 undo + await clickUndoUiButton(page); + await clickUndoUiButton(page); + await clickUndoUiButton(page); + + //Assert there are no items within the canvas + const itemsQtyWithinCanvas_step2 = (await getWithinCanvasItemList(page)) + .length; + + expect(itemsQtyWithinCanvas_step2).toEqual(0); + + //x3 redo + await clickRedoUiButton(page); + await clickRedoUiButton(page); + await clickRedoUiButton(page); + + //Assert there are again 3 items within the canvas + const itemsQtyWithinCanvas_step3 = (await getWithinCanvasItemList(page)) + .length; + expect(itemsQtyWithinCanvas_step3).toEqual(3); + }); +}); diff --git a/public/icons/addlist.svg b/public/icons/addlist.svg new file mode 100644 index 00000000..e8e1e1b4 --- /dev/null +++ b/public/icons/addlist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/alarm.svg b/public/icons/alarm.svg new file mode 100644 index 00000000..a2f17aec --- /dev/null +++ b/public/icons/alarm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/alternativemouse.svg b/public/icons/alternativemouse.svg new file mode 100644 index 00000000..37a674b9 --- /dev/null +++ b/public/icons/alternativemouse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowbendupleft.svg b/public/icons/arrowbendupleft.svg new file mode 100644 index 00000000..8622868d --- /dev/null +++ b/public/icons/arrowbendupleft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowbendupright.svg b/public/icons/arrowbendupright.svg new file mode 100644 index 00000000..16929199 --- /dev/null +++ b/public/icons/arrowbendupright.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowfatdown.svg b/public/icons/arrowfatdown.svg new file mode 100644 index 00000000..3b68d491 --- /dev/null +++ b/public/icons/arrowfatdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowfatleft.svg b/public/icons/arrowfatleft.svg new file mode 100644 index 00000000..bc94d86f --- /dev/null +++ b/public/icons/arrowfatleft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowfatright.svg b/public/icons/arrowfatright.svg new file mode 100644 index 00000000..0000fd9d --- /dev/null +++ b/public/icons/arrowfatright.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowfatup.svg b/public/icons/arrowfatup.svg new file mode 100644 index 00000000..c95f2b8d --- /dev/null +++ b/public/icons/arrowfatup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowsclockwise.svg b/public/icons/arrowsclockwise.svg new file mode 100644 index 00000000..2ac71b81 --- /dev/null +++ b/public/icons/arrowsclockwise.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowscounterclockwise.svg b/public/icons/arrowscounterclockwise.svg new file mode 100644 index 00000000..3756d8d0 --- /dev/null +++ b/public/icons/arrowscounterclockwise.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/autoflash.svg b/public/icons/autoflash.svg new file mode 100644 index 00000000..75495c8e --- /dev/null +++ b/public/icons/autoflash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/calendar.svg b/public/icons/calendar.svg new file mode 100644 index 00000000..7b9270ce --- /dev/null +++ b/public/icons/calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/callphoneincoming.svg b/public/icons/callphoneincoming.svg new file mode 100644 index 00000000..ec93c6e3 --- /dev/null +++ b/public/icons/callphoneincoming.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/camera.svg b/public/icons/camera.svg new file mode 100644 index 00000000..d51fcf35 --- /dev/null +++ b/public/icons/camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/caretdown.svg b/public/icons/caretdown.svg new file mode 100644 index 00000000..6bcca3ef --- /dev/null +++ b/public/icons/caretdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/caretleft.svg b/public/icons/caretleft.svg new file mode 100644 index 00000000..b4bf3c51 --- /dev/null +++ b/public/icons/caretleft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/caretright.svg b/public/icons/caretright.svg new file mode 100644 index 00000000..523c9793 --- /dev/null +++ b/public/icons/caretright.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/caretup.svg b/public/icons/caretup.svg new file mode 100644 index 00000000..5f197bec --- /dev/null +++ b/public/icons/caretup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/check.svg b/public/icons/check.svg new file mode 100644 index 00000000..f3015dcb --- /dev/null +++ b/public/icons/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/checkfat.svg b/public/icons/checkfat.svg new file mode 100644 index 00000000..3a62fb8a --- /dev/null +++ b/public/icons/checkfat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/company.svg b/public/icons/company.svg new file mode 100644 index 00000000..a72ba20c --- /dev/null +++ b/public/icons/company.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/controller.svg b/public/icons/controller.svg new file mode 100644 index 00000000..fbc75e00 --- /dev/null +++ b/public/icons/controller.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/copyright.svg b/public/icons/copyright.svg new file mode 100644 index 00000000..b8838440 --- /dev/null +++ b/public/icons/copyright.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/cursor.svg b/public/icons/cursor.svg new file mode 100644 index 00000000..1776f3bf --- /dev/null +++ b/public/icons/cursor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/cursorclick.svg b/public/icons/cursorclick.svg new file mode 100644 index 00000000..151300e7 --- /dev/null +++ b/public/icons/cursorclick.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/dotssquare.svg b/public/icons/dotssquare.svg new file mode 100644 index 00000000..90e4c2b6 --- /dev/null +++ b/public/icons/dotssquare.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/dotsvertical.svg b/public/icons/dotsvertical.svg new file mode 100644 index 00000000..e78d0c71 --- /dev/null +++ b/public/icons/dotsvertical.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/doublecheck.svg b/public/icons/doublecheck.svg new file mode 100644 index 00000000..0b8babe4 --- /dev/null +++ b/public/icons/doublecheck.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/drive.svg b/public/icons/drive.svg new file mode 100644 index 00000000..adcab6ac --- /dev/null +++ b/public/icons/drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/emptybattery.svg b/public/icons/emptybattery.svg new file mode 100644 index 00000000..8d4cbada --- /dev/null +++ b/public/icons/emptybattery.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/factory.svg b/public/icons/factory.svg new file mode 100644 index 00000000..c41a3fe1 --- /dev/null +++ b/public/icons/factory.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/filedoc.svg b/public/icons/filedoc.svg new file mode 100644 index 00000000..0e59f4fa --- /dev/null +++ b/public/icons/filedoc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/fileexcel.svg b/public/icons/fileexcel.svg new file mode 100644 index 00000000..daf4d734 --- /dev/null +++ b/public/icons/fileexcel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/filejpg.svg b/public/icons/filejpg.svg new file mode 100644 index 00000000..63c73cc0 --- /dev/null +++ b/public/icons/filejpg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/filepdf.svg b/public/icons/filepdf.svg new file mode 100644 index 00000000..db9d41a5 --- /dev/null +++ b/public/icons/filepdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/filepng.svg b/public/icons/filepng.svg new file mode 100644 index 00000000..2b12da29 --- /dev/null +++ b/public/icons/filepng.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/filepowerpoint.svg b/public/icons/filepowerpoint.svg new file mode 100644 index 00000000..9444ea8c --- /dev/null +++ b/public/icons/filepowerpoint.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/firstaid.svg b/public/icons/firstaid.svg new file mode 100644 index 00000000..07581b08 --- /dev/null +++ b/public/icons/firstaid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/flag.svg b/public/icons/flag.svg new file mode 100644 index 00000000..36594a99 --- /dev/null +++ b/public/icons/flag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/flashslash.svg b/public/icons/flashslash.svg new file mode 100644 index 00000000..466efdd1 --- /dev/null +++ b/public/icons/flashslash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/gitlab.svg b/public/icons/gitlab.svg new file mode 100644 index 00000000..bbc69a9c --- /dev/null +++ b/public/icons/gitlab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/hammer.svg b/public/icons/hammer.svg new file mode 100644 index 00000000..110aa5c6 --- /dev/null +++ b/public/icons/hammer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/help.svg b/public/icons/help.svg new file mode 100644 index 00000000..6097ec58 --- /dev/null +++ b/public/icons/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/home.svg b/public/icons/home.svg new file mode 100644 index 00000000..188c3d2d --- /dev/null +++ b/public/icons/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/joystick.svg b/public/icons/joystick.svg new file mode 100644 index 00000000..d5ecc1c1 --- /dev/null +++ b/public/icons/joystick.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/key.svg b/public/icons/key.svg new file mode 100644 index 00000000..b6ec42ae --- /dev/null +++ b/public/icons/key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/keyboard.svg b/public/icons/keyboard.svg new file mode 100644 index 00000000..a7a79892 --- /dev/null +++ b/public/icons/keyboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/lightning.svg b/public/icons/lightning.svg new file mode 100644 index 00000000..88fd6ae3 --- /dev/null +++ b/public/icons/lightning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/linux.svg b/public/icons/linux.svg new file mode 100644 index 00000000..65615d6d --- /dev/null +++ b/public/icons/linux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/listchecks.svg b/public/icons/listchecks.svg new file mode 100644 index 00000000..e447fa2b --- /dev/null +++ b/public/icons/listchecks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/listdashes.svg b/public/icons/listdashes.svg new file mode 100644 index 00000000..102e7a31 --- /dev/null +++ b/public/icons/listdashes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/listdots.svg b/public/icons/listdots.svg new file mode 100644 index 00000000..bfb265e6 --- /dev/null +++ b/public/icons/listdots.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/listheart.svg b/public/icons/listheart.svg new file mode 100644 index 00000000..40ee0693 --- /dev/null +++ b/public/icons/listheart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/listnumbers.svg b/public/icons/listnumbers.svg new file mode 100644 index 00000000..1f72ebab --- /dev/null +++ b/public/icons/listnumbers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/liststar.svg b/public/icons/liststar.svg new file mode 100644 index 00000000..243c4eaf --- /dev/null +++ b/public/icons/liststar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/lock.svg b/public/icons/lock.svg new file mode 100644 index 00000000..6eaba0b9 --- /dev/null +++ b/public/icons/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/lockopen.svg b/public/icons/lockopen.svg new file mode 100644 index 00000000..bc29248f --- /dev/null +++ b/public/icons/lockopen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/microphone.svg b/public/icons/microphone.svg new file mode 100644 index 00000000..be171729 --- /dev/null +++ b/public/icons/microphone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/microphoneslash.svg b/public/icons/microphoneslash.svg new file mode 100644 index 00000000..262343fa --- /dev/null +++ b/public/icons/microphoneslash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/moon.svg b/public/icons/moon.svg new file mode 100644 index 00000000..ba13967b --- /dev/null +++ b/public/icons/moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/musicnote.svg b/public/icons/musicnote.svg new file mode 100644 index 00000000..b77539be --- /dev/null +++ b/public/icons/musicnote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/normalshield.svg b/public/icons/normalshield.svg new file mode 100644 index 00000000..9293db66 --- /dev/null +++ b/public/icons/normalshield.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phone.svg b/public/icons/phone.svg new file mode 100644 index 00000000..6bf536a0 --- /dev/null +++ b/public/icons/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phonecall.svg b/public/icons/phonecall.svg new file mode 100644 index 00000000..fbecdd97 --- /dev/null +++ b/public/icons/phonecall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phonehang.svg b/public/icons/phonehang.svg new file mode 100644 index 00000000..09677754 --- /dev/null +++ b/public/icons/phonehang.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phonelist.svg b/public/icons/phonelist.svg new file mode 100644 index 00000000..57dd209d --- /dev/null +++ b/public/icons/phonelist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phonepause.svg b/public/icons/phonepause.svg new file mode 100644 index 00000000..dbb6a8db --- /dev/null +++ b/public/icons/phonepause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phoneslash.svg b/public/icons/phoneslash.svg new file mode 100644 index 00000000..e7086a36 --- /dev/null +++ b/public/icons/phoneslash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/plug.svg b/public/icons/plug.svg new file mode 100644 index 00000000..2e07385f --- /dev/null +++ b/public/icons/plug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/power.svg b/public/icons/power.svg new file mode 100644 index 00000000..c52b50bb --- /dev/null +++ b/public/icons/power.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/printer.svg b/public/icons/printer.svg new file mode 100644 index 00000000..c2df959d --- /dev/null +++ b/public/icons/printer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/scissors.svg b/public/icons/scissors.svg new file mode 100644 index 00000000..fc8fc248 --- /dev/null +++ b/public/icons/scissors.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/searchlist.svg b/public/icons/searchlist.svg new file mode 100644 index 00000000..d1575f46 --- /dev/null +++ b/public/icons/searchlist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/shieldcheck.svg b/public/icons/shieldcheck.svg new file mode 100644 index 00000000..ee9d4bfb --- /dev/null +++ b/public/icons/shieldcheck.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/shieldcheckered.svg b/public/icons/shieldcheckered.svg new file mode 100644 index 00000000..e0623fd7 --- /dev/null +++ b/public/icons/shieldcheckered.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/shieldslash.svg b/public/icons/shieldslash.svg new file mode 100644 index 00000000..267243bd --- /dev/null +++ b/public/icons/shieldslash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/shieldwarning.svg b/public/icons/shieldwarning.svg new file mode 100644 index 00000000..8c930220 --- /dev/null +++ b/public/icons/shieldwarning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/signin.svg b/public/icons/signin.svg new file mode 100644 index 00000000..4290f3e8 --- /dev/null +++ b/public/icons/signin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/signout.svg b/public/icons/signout.svg new file mode 100644 index 00000000..26c94390 --- /dev/null +++ b/public/icons/signout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/spinner.svg b/public/icons/spinner.svg new file mode 100644 index 00000000..94b6df09 --- /dev/null +++ b/public/icons/spinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/star.svg b/public/icons/star.svg new file mode 100644 index 00000000..d2659c54 --- /dev/null +++ b/public/icons/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/subtitles.svg b/public/icons/subtitles.svg new file mode 100644 index 00000000..075c420d --- /dev/null +++ b/public/icons/subtitles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textaligncenter.svg b/public/icons/textaligncenter.svg new file mode 100644 index 00000000..ed31774d --- /dev/null +++ b/public/icons/textaligncenter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textalignjustify.svg b/public/icons/textalignjustify.svg new file mode 100644 index 00000000..bdddc920 --- /dev/null +++ b/public/icons/textalignjustify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textalignleft.svg b/public/icons/textalignleft.svg new file mode 100644 index 00000000..2b5c720e --- /dev/null +++ b/public/icons/textalignleft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textalignright.svg b/public/icons/textalignright.svg new file mode 100644 index 00000000..365ec8cc --- /dev/null +++ b/public/icons/textalignright.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textindent.svg b/public/icons/textindent.svg new file mode 100644 index 00000000..6b6d648f --- /dev/null +++ b/public/icons/textindent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/upload.svg b/public/icons/upload.svg new file mode 100644 index 00000000..daf15cbe --- /dev/null +++ b/public/icons/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/warning.svg b/public/icons/warning.svg new file mode 100644 index 00000000..7924d15f --- /dev/null +++ b/public/icons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/warningcircle.svg b/public/icons/warningcircle.svg new file mode 100644 index 00000000..f645bd1c --- /dev/null +++ b/public/icons/warningcircle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/webcam.svg b/public/icons/webcam.svg new file mode 100644 index 00000000..a28cd7ea --- /dev/null +++ b/public/icons/webcam.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/webcamslash.svg b/public/icons/webcamslash.svg new file mode 100644 index 00000000..591edab2 --- /dev/null +++ b/public/icons/webcamslash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/windows.svg b/public/icons/windows.svg new file mode 100644 index 00000000..dbf56516 --- /dev/null +++ b/public/icons/windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/shapes/line.svg b/public/shapes/horizontalLine.svg similarity index 83% rename from public/shapes/line.svg rename to public/shapes/horizontalLine.svg index 2c40684d..967d76cd 100644 --- a/public/shapes/line.svg +++ b/public/shapes/horizontalLine.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/public/shapes/verticalLine.svg b/public/shapes/verticalLine.svg new file mode 100644 index 00000000..e8f76fac --- /dev/null +++ b/public/shapes/verticalLine.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/widgets/datepicker.svg b/public/widgets/datepicker.svg index ce18562e..d0bf6e63 100644 --- a/public/widgets/datepicker.svg +++ b/public/widgets/datepicker.svg @@ -1,9 +1,68 @@ - - - + - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/widgets/timepicker.svg b/public/widgets/timepicker.svg index d71771ce..1a262986 100644 --- a/public/widgets/timepicker.svg +++ b/public/widgets/timepicker.svg @@ -1,13 +1,24 @@ - - - - - - - : - - - : - + + + + + Time + + + + hh:mm + + + + + + + + + + + + + + diff --git a/src/common/components/icons/index.ts b/src/common/components/icons/index.ts index 2bf628b8..c5a2d422 100644 --- a/src/common/components/icons/index.ts +++ b/src/common/components/icons/index.ts @@ -8,3 +8,5 @@ export * from './x-icon.component'; export * from './quickmock-logo.component'; export * from './copy-icon.component'; export * from './paste-icon.component'; +export * from './delete-icon.component'; +export * from './pencil-icon.component'; diff --git a/src/common/components/icons/pencil-icon.component.tsx b/src/common/components/icons/pencil-icon.component.tsx new file mode 100644 index 00000000..232a6868 --- /dev/null +++ b/src/common/components/icons/pencil-icon.component.tsx @@ -0,0 +1,15 @@ +export const PencilIcon = () => { + return ( + + + + ); +}; diff --git a/src/common/components/mock-components/front-basic-shapes/line-basic-shape.tsx b/src/common/components/mock-components/front-basic-shapes/horizontal-line-basic-shape.tsx similarity index 81% rename from src/common/components/mock-components/front-basic-shapes/line-basic-shape.tsx rename to src/common/components/mock-components/front-basic-shapes/horizontal-line-basic-shape.tsx index 21600aa2..35b3e729 100644 --- a/src/common/components/mock-components/front-basic-shapes/line-basic-shape.tsx +++ b/src/common/components/mock-components/front-basic-shapes/horizontal-line-basic-shape.tsx @@ -7,7 +7,7 @@ import { useShapeProps } from '../../shapes/use-shape-props.hook'; import { BASIC_SHAPE } from '../front-components/shape.const'; import { useGroupShapeProps } from '../mock-components.utils'; -const lineShapeRestrictions: ShapeSizeRestrictions = { +const horizontalLineShapeRestrictions: ShapeSizeRestrictions = { minWidth: 50, minHeight: 10, maxWidth: -1, @@ -16,12 +16,12 @@ const lineShapeRestrictions: ShapeSizeRestrictions = { defaultHeight: 10, }; -export const getlineShapeRestrictions = (): ShapeSizeRestrictions => - lineShapeRestrictions; +export const getHorizontalLineShapeRestrictions = (): ShapeSizeRestrictions => + horizontalLineShapeRestrictions; -const shapeType: ShapeType = 'line'; +const shapeType: ShapeType = 'horizontalLine'; -export const LineShape = forwardRef((props, ref) => { +export const HorizontalLineShape = forwardRef((props, ref) => { const { x, y, @@ -34,7 +34,7 @@ export const LineShape = forwardRef((props, ref) => { ...shapeProps } = props; const restrictedSize = fitSizeToShapeSizeRestrictions( - lineShapeRestrictions, + horizontalLineShapeRestrictions, width, height ); diff --git a/src/common/components/mock-components/front-basic-shapes/index.ts b/src/common/components/mock-components/front-basic-shapes/index.ts index 602fa0ba..8a8940da 100644 --- a/src/common/components/mock-components/front-basic-shapes/index.ts +++ b/src/common/components/mock-components/front-basic-shapes/index.ts @@ -1,7 +1,8 @@ export * from './rectangle-basic-shape'; export * from './postit-basic-shape'; export * from './diamond-shape'; -export * from './line-basic-shape'; +export * from './horizontal-line-basic-shape'; +export * from './vertical-line-basic-shape'; export * from './triangle-basic-shape'; export * from './circle-basic-shape'; export * from './star-shape'; diff --git a/src/common/components/mock-components/front-basic-shapes/vertical-line-basic-shape.tsx b/src/common/components/mock-components/front-basic-shapes/vertical-line-basic-shape.tsx new file mode 100644 index 00000000..5cdf01e0 --- /dev/null +++ b/src/common/components/mock-components/front-basic-shapes/vertical-line-basic-shape.tsx @@ -0,0 +1,72 @@ +import { forwardRef } from 'react'; +import { Group, Line, Rect } from 'react-konva'; +import { ShapeSizeRestrictions, ShapeType } from '@/core/model'; +import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes/shape-restrictions'; +import { ShapeProps } from '../shape.model'; +import { useShapeProps } from '../../shapes/use-shape-props.hook'; +import { BASIC_SHAPE } from '../front-components/shape.const'; +import { useGroupShapeProps } from '../mock-components.utils'; + +const verticalLineShapeRestrictions: ShapeSizeRestrictions = { + minWidth: 10, + minHeight: 50, + maxWidth: 10, + maxHeight: -1, + defaultWidth: 10, + defaultHeight: 200, +}; + +export const getVerticalLineShapeRestrictions = (): ShapeSizeRestrictions => + verticalLineShapeRestrictions; + +const shapeType: ShapeType = 'verticalLine'; + +export const VerticalLineShape = forwardRef((props, ref) => { + const { + x, + y, + width, + height, + id, + onSelected, + text, + otherProps, + ...shapeProps + } = props; + const restrictedSize = fitSizeToShapeSizeRestrictions( + verticalLineShapeRestrictions, + width, + height + ); + + const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; + + const { stroke, strokeStyle } = useShapeProps(otherProps, BASIC_SHAPE); + + const commonGroupProps = useGroupShapeProps( + props, + restrictedSize, + shapeType, + ref + ); + + return ( + + {/* Transparent rectangle for applying margin */} + + + + + ); +}); diff --git a/src/common/components/mock-components/front-components/button-shape.tsx b/src/common/components/mock-components/front-components/button-shape.tsx index a24f8a34..73c3941e 100644 --- a/src/common/components/mock-components/front-components/button-shape.tsx +++ b/src/common/components/mock-components/front-components/button-shape.tsx @@ -11,7 +11,7 @@ const buttonShapeRestrictions: ShapeSizeRestrictions = { minWidth: 50, minHeight: 35, maxWidth: -1, - maxHeight: 35, + maxHeight: 100, defaultWidth: 100, defaultHeight: 35, }; @@ -68,9 +68,9 @@ export const ButtonShape = forwardRef((props, ref) => { /> ( (props, ref) => { - const { x, y, width, height, id, onSelected, otherProps, ...shapeProps } = - props; + const { + x, + y, + width, + height, + id, + onSelected, + text, + otherProps, + ...shapeProps + } = props; const restrictedSize = fitSizeToShapeSizeRestrictions( datepickerInputShapeRestrictions, width, @@ -33,14 +44,8 @@ export const DatepickerInputShape = forwardRef( const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - const separatorPadding = 12; - const separator1X = restrictedWidth / 3; - const separator2X = (2 * restrictedWidth) / 3; - - const { stroke, strokeStyle, fill, borderRadius } = useShapeProps( - otherProps, - BASIC_SHAPE - ); + const { stroke, fill, textColor, strokeStyle, borderRadius } = + useShapeProps(otherProps, INPUT_SHAPE); const commonGroupProps = useGroupShapeProps( props, @@ -49,40 +54,80 @@ export const DatepickerInputShape = forwardRef( ref ); + const iconWidth = 25; + const availableWidth = restrictedWidth - iconWidth - 20; + const fontSize = Math.min( + availableWidth * 0.2, + restrictedHeight * 0.35, + 20 + ); + const labelFontSize = Math.min(restrictedHeight * 0.3, 12); + + const calendarIcon = new window.Image(); + calendarIcon.src = calendarIconSrc; + return ( - {/* input frame */} + {/* External Rectangle */} - {/* Inverted diagonal spacers */} - - + {/* Main Text */} + + + + {/* Calendar Icon */} + ); diff --git a/src/common/components/mock-components/front-components/icon/icon-shape.business.ts b/src/common/components/mock-components/front-components/icon/icon-shape.business.ts new file mode 100644 index 00000000..9378b3e3 --- /dev/null +++ b/src/common/components/mock-components/front-components/icon/icon-shape.business.ts @@ -0,0 +1,33 @@ +import { IconSize } from '@/core/model'; + +export const loadSvgWithFill = async (url: string, fillColor: string) => { + const response = await fetch(url); + const svgText = await response.text(); + + const modifiedSvg = svgText.replace(/fill="[^"]*"/g, `fill="${fillColor}"`); + + const svgBlob = new Blob([modifiedSvg], { type: 'image/svg+xml' }); + const objectURL = URL.createObjectURL(svgBlob); + + const img = new window.Image(); + img.src = objectURL; + + return img; +}; + +export const returnIconSize = (iconSize: IconSize): number[] => { + switch (iconSize) { + case 'XS': + return [25, 25]; + case 'S': + return [50, 50]; + case 'M': + return [100, 100]; + case 'L': + return [125, 125]; + case 'XL': + return [150, 150]; + default: + return [50, 50]; + } +}; diff --git a/src/common/components/mock-components/front-components/icon-shape.tsx b/src/common/components/mock-components/front-components/icon/icon-shape.tsx similarity index 66% rename from src/common/components/mock-components/front-components/icon-shape.tsx rename to src/common/components/mock-components/front-components/icon/icon-shape.tsx index 6e3936e2..f268a9ae 100644 --- a/src/common/components/mock-components/front-components/icon-shape.tsx +++ b/src/common/components/mock-components/front-components/icon/icon-shape.tsx @@ -1,18 +1,13 @@ import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes/shape-restrictions'; -import { - BASE_ICONS_URL, - IconSize, - ShapeSizeRestrictions, - ShapeType, -} from '@/core/model'; -import { forwardRef } from 'react'; +import { BASE_ICONS_URL, ShapeSizeRestrictions, ShapeType } from '@/core/model'; +import { forwardRef, useRef, useState, useEffect } from 'react'; import { Group, Image } from 'react-konva'; -import useImage from 'use-image'; -import { ShapeProps } from '../shape.model'; +import { ShapeProps } from '../../shape.model'; import { useModalDialogContext } from '@/core/providers/model-dialog-providers/model-dialog.provider'; import { IconModal } from '@/pods/properties/components/icon-selector/modal'; import { useCanvasContext } from '@/core/providers'; -import { useGroupShapeProps } from '../mock-components.utils'; +import { useGroupShapeProps } from '../../mock-components.utils'; +import { loadSvgWithFill, returnIconSize } from './icon-shape.business'; const iconShapeRestrictions: ShapeSizeRestrictions = { minWidth: 25, @@ -38,11 +33,13 @@ export const SvgIcon = forwardRef((props, ref) => { onSelected, iconInfo, iconSize, + stroke, ...shapeProps } = props; const { openModal } = useModalDialogContext(); const { selectionInfo } = useCanvasContext(); const { updateOtherPropsOnSelected } = selectionInfo; + const handleDoubleClick = () => { openModal( ((props, ref) => { 'Choose Icon' ); }; - const [image] = useImage(`${BASE_ICONS_URL}${iconInfo.filename}`); - - const returnIconSize = (iconSize: IconSize): number[] => { - switch (iconSize) { - case 'XS': - return [25, 25]; - case 'S': - return [50, 50]; - case 'M': - return [100, 100]; - case 'L': - return [125, 125]; - case 'XL': - return [150, 150]; - default: - return [50, 50]; - } - }; const [iconWidth, iconHeight] = returnIconSize(iconSize); @@ -88,15 +67,32 @@ export const SvgIcon = forwardRef((props, ref) => { ref ); + const [image, setImage] = useState(null); + const imageRef = useRef(null); + + useEffect(() => { + if (iconInfo?.filename) { + loadSvgWithFill( + `${BASE_ICONS_URL}${iconInfo.filename}`, + `${stroke}` + ).then(img => { + setImage(img); + }); + } + }, [iconInfo?.filename, stroke]); + return ( - + {image && ( + + )} ); }); diff --git a/src/common/components/mock-components/front-components/icon/index.ts b/src/common/components/mock-components/front-components/icon/index.ts new file mode 100644 index 00000000..076f313a --- /dev/null +++ b/src/common/components/mock-components/front-components/icon/index.ts @@ -0,0 +1 @@ +export * from './icon-shape'; diff --git a/src/common/components/mock-components/front-components/index.ts b/src/common/components/mock-components/front-components/index.ts index ae303ff1..f79d76ca 100644 --- a/src/common/components/mock-components/front-components/index.ts +++ b/src/common/components/mock-components/front-components/index.ts @@ -7,9 +7,9 @@ export * from './progressbar-shape'; export * from './listbox'; export * from './datepickerinput-shape'; export * from './button-shape'; -export * from './timepickerinput-shape'; +export * from './timepickerinput/timepickerinput-shape'; export * from './radiobutton-shape'; -export * from './icon-shape'; +export * from './icon'; export * from './verticalscrollbar-shape'; export * from './horizontalscrollbar-shape'; export * from './label-shape'; diff --git a/src/common/components/mock-components/front-components/input-shape.tsx b/src/common/components/mock-components/front-components/input-shape.tsx index 2c415044..959c66e8 100644 --- a/src/common/components/mock-components/front-components/input-shape.tsx +++ b/src/common/components/mock-components/front-components/input-shape.tsx @@ -82,3 +82,33 @@ export const InputShape = forwardRef((props, ref) => { ); }); + +/* + + + + + +*/ diff --git a/src/common/components/mock-components/front-components/shape.const.ts b/src/common/components/mock-components/front-components/shape.const.ts index b3d155ba..f35a775e 100644 --- a/src/common/components/mock-components/front-components/shape.const.ts +++ b/src/common/components/mock-components/front-components/shape.const.ts @@ -3,7 +3,7 @@ const DEFAULT_STROKE_COLOR = '#000000'; const DEFAULT_STROKE_WIDTH = 1; const DEFAULT_STROKE_STYLE: number[] = []; const DEFAULT_FILL_BACKGROUND = '#ffffff'; -const DEFAULT_FONT_FAMILY = 'Comic Sans MS, Balsamiq Sans, cursive'; +const DEFAULT_FONT_FAMILY = 'Balsamiq Sans, Comic Sans MS, cursive'; const DEFAULT_FONT_SIZE = 16; const DEFAULT_FILL_TEXT = '#000000'; const DEFAULT_PADDING = 10; diff --git a/src/common/components/mock-components/front-components/timepickerinput-shape.tsx b/src/common/components/mock-components/front-components/timepickerinput-shape.tsx deleted file mode 100644 index a4b64adc..00000000 --- a/src/common/components/mock-components/front-components/timepickerinput-shape.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { ShapeSizeRestrictions, ShapeType } from '@/core/model'; -import { forwardRef } from 'react'; -import { ShapeProps } from '../shape.model'; -import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes/shape-restrictions'; -import { Group, Rect, Text } from 'react-konva'; -import { BASIC_SHAPE } from './shape.const'; -import { useShapeProps } from '../../shapes/use-shape-props.hook'; -import { useGroupShapeProps } from '../mock-components.utils'; - -const timepickerInputShapeRestrictions: ShapeSizeRestrictions = { - minWidth: 100, - minHeight: 50, - maxWidth: -1, - maxHeight: 50, - defaultWidth: 220, - defaultHeight: 50, -}; - -const shapeType: ShapeType = 'timepickerinput'; - -export const getTimepickerInputShapeSizeRestrictions = - (): ShapeSizeRestrictions => timepickerInputShapeRestrictions; - -export const TimepickerInputShape = forwardRef( - (props, ref) => { - const { x, y, width, height, id, onSelected, otherProps, ...shapeProps } = - props; - const restrictedSize = fitSizeToShapeSizeRestrictions( - timepickerInputShapeRestrictions, - width, - height - ); - const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - - const separatorPadding = 3; // Extra padding for spacers - const separator1X = restrictedWidth / 3; - const separator2X = (2 * restrictedWidth) / 3; - - const { stroke, strokeStyle, fill, borderRadius } = useShapeProps( - otherProps, - BASIC_SHAPE - ); - - const commonGroupProps = useGroupShapeProps( - props, - restrictedSize, - shapeType, - ref - ); - - return ( - - {/* input frame */} - - - {/* Separators : */} - - - - ); - } -); diff --git a/src/common/components/mock-components/front-components/timepickerinput/index.ts b/src/common/components/mock-components/front-components/timepickerinput/index.ts new file mode 100644 index 00000000..b2663481 --- /dev/null +++ b/src/common/components/mock-components/front-components/timepickerinput/index.ts @@ -0,0 +1 @@ +export * from './timepickerinput-shape.tsx'; diff --git a/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.business.ts b/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.business.ts new file mode 100644 index 00000000..a257c096 --- /dev/null +++ b/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.business.ts @@ -0,0 +1,33 @@ +const MAX_DIGITS = 2; +const MAX_HOURS = '23'; +const MAX_MINUTES = '59'; +const HOUR_MASK = 'hh'; +const MINUTES_MASK = 'mm'; + +export const splitCSVContent = (csvContent: string): string[] => { + const splitedCsvContent = csvContent + .trim() + .split(/[:|,]/) + .map(el => el.trim()); + return splitedCsvContent; +}; + +export const setTime = (csvData: string[]) => { + let [hour, minutes] = csvData; + if (csvData.length < 2) { + return true; + } + if (csvData[0] !== HOUR_MASK || csvData[1] !== MINUTES_MASK) { + if ( + csvData.length > MAX_DIGITS || + hour.length !== MAX_DIGITS || + hour === '' || + hour > MAX_HOURS || + minutes.length !== MAX_DIGITS || + minutes === '' || + minutes > MAX_MINUTES + ) { + return true; + } + } +}; diff --git a/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.tsx b/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.tsx new file mode 100644 index 00000000..a0ec43c8 --- /dev/null +++ b/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.tsx @@ -0,0 +1,148 @@ +import { ShapeSizeRestrictions, ShapeType } from '@/core/model'; +import { forwardRef } from 'react'; +import { ShapeProps } from '../../shape.model'; +import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes/shape-restrictions'; +import { Group, Rect, Text, Image } from 'react-konva'; +import { BASIC_SHAPE } from '../shape.const'; +import { useShapeProps } from '../../../shapes/use-shape-props.hook'; +import { useGroupShapeProps } from '../../mock-components.utils'; +import { splitCSVContent, setTime } from './timepickerinput-shape.business'; + +import clockIconSrc from '/icons/clock.svg'; + +const timepickerInputShapeRestrictions: ShapeSizeRestrictions = { + minWidth: 100, + minHeight: 50, + maxWidth: -1, + maxHeight: 50, + defaultWidth: 220, + defaultHeight: 50, +}; + +const shapeType: ShapeType = 'timepickerinput'; + +export const getTimepickerInputShapeSizeRestrictions = + (): ShapeSizeRestrictions => timepickerInputShapeRestrictions; + +export const TimepickerInputShape = forwardRef( + (props, ref) => { + const { + x, + y, + width, + height, + id, + text, + onSelected, + otherProps, + ...shapeProps + } = props; + + const restrictedSize = fitSizeToShapeSizeRestrictions( + timepickerInputShapeRestrictions, + width, + height + ); + const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; + + const { stroke, strokeStyle, fill, borderRadius } = useShapeProps( + otherProps, + BASIC_SHAPE + ); + + const commonGroupProps = useGroupShapeProps( + props, + restrictedSize, + shapeType, + ref + ); + + const csvData = splitCSVContent(text); + let isError = setTime(csvData); + + const iconWidth = 25; + const availableWidth = restrictedWidth - iconWidth - 20; + const fontSize = Math.min( + availableWidth * 0.2, + restrictedHeight * 0.35, + 20 + ); + const labelFontSize = Math.min(restrictedHeight * 0.3, 12); + + const clockIcon = new window.Image(); + clockIcon.src = clockIconSrc; + + return ( + + {/* External Rectangle */} + + {/* Background of Time Label */} + + {/* Label "Time" */} + + {/* Main Text */} + + {/* Error Text */} + {isError && ( + + )} + + {/* Clock Icon */} + + + ); + } +); diff --git a/src/common/components/mock-components/front-containers/browserwindow-shape.tsx b/src/common/components/mock-components/front-containers/browserwindow-shape.tsx index 524d8937..a40eca77 100644 --- a/src/common/components/mock-components/front-containers/browserwindow-shape.tsx +++ b/src/common/components/mock-components/front-containers/browserwindow-shape.tsx @@ -21,7 +21,7 @@ export const getBrowserWindowShapeSizeRestrictions = const shapeType: ShapeType = 'browser'; export const BrowserWindowShape = forwardRef((props, ref) => { - const { x, y, width, height, id, onSelected, ...shapeProps } = props; + const { x, y, width, height, id, onSelected, text, ...shapeProps } = props; const restrictedSize = fitSizeToShapeSizeRestrictions( browserWindowShapeSizeRestrictions, width, @@ -114,9 +114,9 @@ export const BrowserWindowShape = forwardRef((props, ref) => { @@ -61,9 +61,7 @@ export const ParagraphShape = forwardRef((props, ref) => { fontSize={14} fill={textColor} align="left" - verticalAlign="middle" ellipsis={true} - wrap="none" /> ); diff --git a/src/common/components/mock-components/front-text-components/smalltext-shape.tsx b/src/common/components/mock-components/front-text-components/smalltext-shape.tsx index 4d2da7bd..aa392a5f 100644 --- a/src/common/components/mock-components/front-text-components/smalltext-shape.tsx +++ b/src/common/components/mock-components/front-text-components/smalltext-shape.tsx @@ -8,7 +8,7 @@ import { BASIC_SHAPE } from '../front-components/shape.const'; import { useGroupShapeProps } from '../mock-components.utils'; const smalltextSizeRestrictions: ShapeSizeRestrictions = { - minWidth: 150, + minWidth: 40, minHeight: 20, maxWidth: -1, maxHeight: -1, diff --git a/src/core/local-disk/shapes-to-document.mapper.ts b/src/core/local-disk/shapes-to-document.mapper.ts index 2d01dbbe..e32bbe69 100644 --- a/src/core/local-disk/shapes-to-document.mapper.ts +++ b/src/core/local-disk/shapes-to-document.mapper.ts @@ -1,36 +1,46 @@ import { ShapeModel } from '../model'; import { DocumentModel } from '../providers/canvas/canvas.model'; -import { QuickMockFileContract, Page } from './local-disk.model'; +import { QuickMockFileContract } from './local-disk.model'; export const mapFromShapesArrayToQuickMockFileDocument = ( - shapes: ShapeModel[] + fullDocument: DocumentModel ): QuickMockFileContract => { - const pages: Page[] = shapes.reduce((acc, shape) => { - /* - * TODO: Add the correct id, name and version values. - */ - const newPage: Page = { - id: '1', - name: 'default', - shapes: [{ ...shape }], - }; - - return [...acc, newPage]; - }, [] as Page[]); - + // TODO: Serialize the activePageIndex? return { - version: '0.1', - pages, + version: '0.2', + pages: fullDocument.pages, }; }; export const mapFromQuickMockFileDocumentToApplicationDocument = ( fileDocument: QuickMockFileContract ): DocumentModel => { - const shapes: ShapeModel[] = fileDocument.pages.reduce((acc, page) => { - return [...acc, ...page.shapes]; - }, [] as ShapeModel[]); return { - shapes, + activePageIndex: 0, + pages: fileDocument.pages, + }; +}; + +// Example function to handle version 0.1 parsing +export const mapFromQuickMockFileDocumentToApplicationDocumentV0_1 = ( + fileDocument: QuickMockFileContract +): DocumentModel => { + // Combine all shapes into a single page + const combinedShapes: ShapeModel[] = fileDocument.pages.reduce( + (acc: ShapeModel[], page) => { + return acc.concat(page.shapes); + }, + [] + ); + + return { + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'default', + shapes: combinedShapes, + }, + ], }; }; diff --git a/src/core/local-disk/shapes-to.document.mapper.spec.ts b/src/core/local-disk/shapes-to.document.mapper.spec.ts index f5c943cb..f5b713af 100644 --- a/src/core/local-disk/shapes-to.document.mapper.spec.ts +++ b/src/core/local-disk/shapes-to.document.mapper.spec.ts @@ -1,6 +1,7 @@ import { mapFromShapesArrayToQuickMockFileDocument, mapFromQuickMockFileDocumentToApplicationDocument, + mapFromQuickMockFileDocumentToApplicationDocumentV0_1, } from './shapes-to-document.mapper'; import { ShapeModel } from '../model'; import { QuickMockFileContract } from './local-disk.model'; @@ -8,20 +9,6 @@ import { DocumentModel } from '../providers/canvas/canvas.model'; describe('shapes to document mapper', () => { describe('mapFromShapesArrayToQuickMockFileDocument', () => { - it('Should return a ShapeModel with empty pages', () => { - // Arrange - const shapes: ShapeModel[] = []; - const expectedResult: QuickMockFileContract = { - version: '0.1', - pages: [], - }; - // Act - const result = mapFromShapesArrayToQuickMockFileDocument(shapes); - - // Assert - expect(result).toEqual(expectedResult); - }); - it('Should return a ShapeModel with one pages and shapes', () => { // Arrange const shapes: ShapeModel[] = [ @@ -36,29 +23,30 @@ describe('shapes to document mapper', () => { typeOfTransformer: ['rotate'], }, ]; + + const document: DocumentModel = { + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'default', + shapes: shapes, + }, + ], + }; + const expectedResult: QuickMockFileContract = { - version: '0.1', + version: '0.2', pages: [ { id: '1', name: 'default', - shapes: [ - { - id: '1', - x: 0, - y: 0, - width: 100, - height: 100, - type: 'rectangle', - allowsInlineEdition: false, - typeOfTransformer: ['rotate'], - }, - ], + shapes: shapes, }, ], }; // Act - const result = mapFromShapesArrayToQuickMockFileDocument(shapes); + const result = mapFromShapesArrayToQuickMockFileDocument(document); // Assert expect(result).toEqual(expectedResult); @@ -88,45 +76,31 @@ describe('shapes to document mapper', () => { typeOfTransformer: ['rotate'], }, ]; - const expectedResult: QuickMockFileContract = { - version: '0.1', + + const document: DocumentModel = { + activePageIndex: 0, pages: [ { id: '1', name: 'default', - shapes: [ - { - id: '1', - x: 0, - y: 0, - width: 100, - height: 100, - type: 'rectangle', - allowsInlineEdition: false, - typeOfTransformer: ['rotate'], - }, - ], + shapes: shapes, }, + ], + }; + + const expectedResult: QuickMockFileContract = { + version: '0.2', + pages: [ { id: '1', name: 'default', - shapes: [ - { - id: '2', - x: 0, - y: 0, - width: 100, - height: 100, - type: 'circle', - allowsInlineEdition: true, - typeOfTransformer: ['rotate'], - }, - ], + shapes: shapes, }, ], }; + // Act - const result = mapFromShapesArrayToQuickMockFileDocument(shapes); + const result = mapFromShapesArrayToQuickMockFileDocument(document); // Assert expect(result).toEqual(expectedResult); @@ -137,11 +111,25 @@ describe('shapes to document mapper', () => { it('Should return a document model with a empty shapes array when we feed a empty pages array', () => { //arrange const fileDocument: QuickMockFileContract = { - version: '0.1', - pages: [], + version: '0.2', + pages: [ + { + id: '1', + name: 'default', + shapes: [], + }, + ], }; + const expectedResult: DocumentModel = { - shapes: [], + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'default', + shapes: [], + }, + ], }; //act const result = @@ -153,7 +141,7 @@ describe('shapes to document mapper', () => { it('Should return a document model with a empty shapes array when we feed a file document with a one page but with empty shapes', () => { //arrange const fileDocument: QuickMockFileContract = { - version: '0.1', + version: '0.2', pages: [ { id: '1', @@ -162,9 +150,18 @@ describe('shapes to document mapper', () => { }, ], }; + const expectedResult: DocumentModel = { - shapes: [], + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'default', + shapes: [], + }, + ], }; + //act const result = mapFromQuickMockFileDocumentToApplicationDocument(fileDocument); @@ -195,20 +192,29 @@ describe('shapes to document mapper', () => { }, ], }; + const expectedResult: DocumentModel = { - shapes: [ + activePageIndex: 0, + pages: [ { id: '1', - type: 'rectangle', - x: 0, - y: 0, - width: 100, - height: 100, - allowsInlineEdition: false, - typeOfTransformer: ['rotate'], + name: 'default', + shapes: [ + { + id: '1', + type: 'rectangle', + x: 0, + y: 0, + width: 100, + height: 100, + allowsInlineEdition: false, + typeOfTransformer: ['rotate'], + }, + ], }, ], }; + //act const result = mapFromQuickMockFileDocumentToApplicationDocument(fileDocument); @@ -218,72 +224,131 @@ describe('shapes to document mapper', () => { it('Should return a document model with shapes when we feed a file document with two pages and shapes', () => { //arrange + const shapespageA: ShapeModel[] = [ + { + id: '1', + type: 'rectangle', + x: 0, + y: 0, + width: 100, + height: 100, + allowsInlineEdition: false, + typeOfTransformer: ['rotate'], + }, + ]; + + const shapesPageB: ShapeModel[] = [ + { + id: '3', + type: 'browser', + x: 0, + y: 0, + width: 100, + height: 100, + allowsInlineEdition: true, + typeOfTransformer: [' rotate'], + }, + ]; + const fileDocument: QuickMockFileContract = { version: '0.1', pages: [ { id: '1', name: 'default', - shapes: [ - { - id: '1', - type: 'rectangle', - x: 0, - y: 0, - width: 100, - height: 100, - allowsInlineEdition: false, - typeOfTransformer: ['rotate'], - }, - ], + shapes: shapespageA, }, { id: '2', name: 'default', - shapes: [ - { - id: '3', - type: 'browser', - x: 0, - y: 0, - width: 100, - height: 100, - allowsInlineEdition: true, - typeOfTransformer: [' rotate'], - }, - ], + shapes: shapesPageB, }, ], }; + const expectedResult: DocumentModel = { - shapes: [ + activePageIndex: 0, + pages: [ { id: '1', - type: 'rectangle', - x: 0, - y: 0, - width: 100, - height: 100, - allowsInlineEdition: false, - typeOfTransformer: ['rotate'], + name: 'default', + shapes: shapespageA, }, { - id: '3', - type: 'browser', - x: 0, - y: 0, - width: 100, - height: 100, - allowsInlineEdition: true, - typeOfTransformer: [' rotate'], + id: '2', + name: 'default', + shapes: shapesPageB, }, ], }; + //act const result = mapFromQuickMockFileDocumentToApplicationDocument(fileDocument); //assert expect(result).toEqual(expectedResult); }); + + it('Should return a document model with shapes in one page when we feed a file document from version 0.1', () => { + //arrange + const shapespageA: ShapeModel[] = [ + { + id: '1', + type: 'rectangle', + x: 0, + y: 0, + width: 100, + height: 100, + allowsInlineEdition: false, + typeOfTransformer: ['rotate'], + }, + ]; + + const shapesPageB: ShapeModel[] = [ + { + id: '3', + type: 'browser', + x: 0, + y: 0, + width: 100, + height: 100, + allowsInlineEdition: true, + typeOfTransformer: [' rotate'], + }, + ]; + + const fileDocument: QuickMockFileContract = { + version: '0.1', + pages: [ + { + id: '1', + name: 'default', + shapes: shapespageA, + }, + { + id: '2', + name: 'default', + shapes: shapesPageB, + }, + ], + }; + + const expectedResult: DocumentModel = { + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'default', + shapes: shapespageA.concat(shapesPageB), + }, + ], + }; + + //act + const result = + mapFromQuickMockFileDocumentToApplicationDocumentV0_1(fileDocument); + //assert + expect(result).toEqual(expectedResult); + }); }); }); diff --git a/src/core/local-disk/use-local-disk.hook.ts b/src/core/local-disk/use-local-disk.hook.ts index eea12115..1d5bd4ea 100644 --- a/src/core/local-disk/use-local-disk.hook.ts +++ b/src/core/local-disk/use-local-disk.hook.ts @@ -3,6 +3,7 @@ import { useCanvasContext } from '../providers'; import { mapFromShapesArrayToQuickMockFileDocument, mapFromQuickMockFileDocumentToApplicationDocument, + mapFromQuickMockFileDocumentToApplicationDocumentV0_1, } from './shapes-to-document.mapper'; import { fileInput, OnFileSelectedCallback } from '@/common/file-input'; import { QuickMockFileContract } from './local-disk.model'; @@ -12,10 +13,12 @@ const DEFAULT_FILE_EXTENSION = 'qm'; const DEFAULT_EXTENSION_DESCRIPTION = 'quick mock'; export const useLocalDisk = () => { - const { shapes, loadDocument } = useCanvasContext(); + const { fullDocument, loadDocument, fileName, setFileName } = + useCanvasContext(); const serializeShapes = (): string => { - const quickMockDocument = mapFromShapesArrayToQuickMockFileDocument(shapes); + const quickMockDocument = + mapFromShapesArrayToQuickMockFileDocument(fullDocument); return JSON.stringify(quickMockDocument); }; @@ -38,11 +41,11 @@ export const useLocalDisk = () => { }, content ); - console.log('saveFilename', savedFilename); + setFileName(savedFilename); }; const handleSave = () => { - const filename = DEFAULT_FILE_NAME; + const filename = fileName !== '' ? fileName : DEFAULT_FILE_NAME; const content = serializeShapes(); if ((window as any).showDirectoryPicker === undefined) { OldBrowsersDownloadFile(filename, content); @@ -55,9 +58,18 @@ export const useLocalDisk = () => { reader.onload = () => { const content = reader.result as string; const parseData: QuickMockFileContract = JSON.parse(content); - const appDocument = - mapFromQuickMockFileDocumentToApplicationDocument(parseData); - loadDocument(appDocument); + + if (parseData.version === '0.1') { + // Handle version 0.1 parsing + const appDocument = + mapFromQuickMockFileDocumentToApplicationDocumentV0_1(parseData); + loadDocument(appDocument); + } else { + // Handle other versions + const appDocument = + mapFromQuickMockFileDocumentToApplicationDocument(parseData); + loadDocument(appDocument); + } }; reader.readAsText(file); }; diff --git a/src/core/model/index.ts b/src/core/model/index.ts index cebb04e3..2e68160b 100644 --- a/src/core/model/index.ts +++ b/src/core/model/index.ts @@ -32,7 +32,8 @@ export type ShapeType = | 'audioPlayer' | 'diamond' | 'icon' - | 'line' + | 'horizontalLine' + | 'verticalLine' | 'accordion' | 'pie' | 'horizontal-menu' @@ -90,7 +91,8 @@ export const ShapeDisplayName: Record = { videoPlayer: 'Video Player', audioPlayer: 'Audio Player', diamond: 'Diamond', - line: 'Line', + horizontalLine: 'Horizontal Line', + verticalLine: 'Vertical Line', accordion: 'Accordion', pie: 'Pie', breadcrumb: 'Breadcrumb', diff --git a/src/core/providers/canvas/canvas.business.ts b/src/core/providers/canvas/canvas.business.ts index 2458e7db..be5a6b9e 100644 --- a/src/core/providers/canvas/canvas.business.ts +++ b/src/core/providers/canvas/canvas.business.ts @@ -1,4 +1,5 @@ import { ShapeModel } from '@/core/model'; +import { DocumentModel } from './canvas.model'; export const removeShapesFromList = ( shapeIds: string[], @@ -10,3 +11,18 @@ export const removeShapesFromList = ( return shapeCollection.filter(shape => !shapeIds.includes(shape.id)); }; + +export const isPageIndexValid = (document: DocumentModel) => { + return ( + document.activePageIndex !== -1 && + document.activePageIndex < document.pages.length + ); +}; + +export const getActivePageShapes = (document: DocumentModel) => { + if (!isPageIndexValid(document)) { + return []; + } + + return document.pages[document.activePageIndex].shapes; +}; diff --git a/src/core/providers/canvas/canvas.model.ts b/src/core/providers/canvas/canvas.model.ts index 844ab71d..0337290d 100644 --- a/src/core/providers/canvas/canvas.model.ts +++ b/src/core/providers/canvas/canvas.model.ts @@ -11,6 +11,28 @@ import { Node, NodeConfig } from 'konva/lib/Node'; export type ZIndexAction = 'top' | 'bottom' | 'up' | 'down'; +export interface Page { + id: string; + name: string; + shapes: ShapeModel[]; +} + +export interface DocumentModel { + pages: Page[]; + activePageIndex: number; +} + +export const createDefaultDocumentModel = (): DocumentModel => ({ + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'Page 1', + shapes: [], + }, + ], +}); + export interface SelectionInfo { transformerRef: React.RefObject; shapeRefs: React.MutableRefObject; @@ -24,6 +46,7 @@ export interface SelectionInfo { | Konva.KonvaEventObject | Konva.KonvaEventObject ) => void; + clearSelection: () => void; selectedShapesRefs: React.MutableRefObject[] | null>; selectedShapesIds: string[]; selectedShapeType: ShapeType | null; @@ -40,7 +63,7 @@ export interface SelectionInfo { export interface CanvasContextModel { shapes: ShapeModel[]; scale: number; - clearCanvas: () => void; + createNewFullDocument: () => void; setScale: React.Dispatch>; addNewShape: ( type: ShapeType, @@ -69,12 +92,16 @@ export interface CanvasContextModel { loadDocument: (document: DocumentModel) => void; isInlineEditing: boolean; setIsInlineEditing: React.Dispatch>; + fileName: string; + setFileName: (fileName: string) => void; + fullDocument: DocumentModel; + addNewPage: () => void; + duplicatePage: (pageIndex: number) => void; + setActivePage: (pageId: string) => void; + deletePage: (pageIndex: number) => void; + editPageTitle: (pageIndex: number, newName: string) => void; + isThumbnailContextMenuVisible: boolean; + setIsThumbnailContextMenuVisible: React.Dispatch< + React.SetStateAction + >; } - -export interface DocumentModel { - shapes: ShapeModel[]; -} - -export const createDefaultDocumentModel = (): DocumentModel => ({ - shapes: [], -}); diff --git a/src/core/providers/canvas/canvas.provider.tsx b/src/core/providers/canvas/canvas.provider.tsx index dcfa6c4c..8e27e500 100644 --- a/src/core/providers/canvas/canvas.provider.tsx +++ b/src/core/providers/canvas/canvas.provider.tsx @@ -8,8 +8,9 @@ import { useStateWithInterceptor } from './canvas.hook'; import { createDefaultDocumentModel, DocumentModel } from './canvas.model'; import { v4 as uuidv4 } from 'uuid'; import Konva from 'konva'; -import { removeShapesFromList } from './canvas.business'; +import { isPageIndexValid, removeShapesFromList } from './canvas.business'; import { useClipboard } from './use-clipboard.hook'; +import { produce } from 'immer'; interface Props { children: React.ReactNode; @@ -21,6 +22,9 @@ export const CanvasProvider: React.FC = props => { const [scale, setScale] = React.useState(1); const stageRef = React.useRef(null); const [isInlineEditing, setIsInlineEditing] = React.useState(false); + const [fileName, setFileName] = React.useState(''); + const [isThumbnailContextMenuVisible, setIsThumbnailContextMenuVisible] = + React.useState(false); const { addSnapshot, @@ -39,16 +43,92 @@ export const CanvasProvider: React.FC = props => { const selectionInfo = useSelection(document, setDocument); + const addNewPage = () => { + setDocument(lastDocument => + produce(lastDocument, draft => { + draft.pages.push({ + id: uuidv4(), + name: `Page ${draft.pages.length + 1}`, + shapes: [], + }); + }) + ); + }; + + const duplicatePage = (pageIndex: number) => { + const newShapes: ShapeModel[] = document.pages[pageIndex].shapes.map( + shape => { + const newShape: ShapeModel = { ...shape }; + newShape.id = uuidv4(); + return newShape; + } + ); + + setDocument(lastDocument => + produce(lastDocument, draft => { + const newPage = { + id: uuidv4(), + name: `${document.pages[pageIndex].name} - copy`, + shapes: newShapes, + }; + draft.pages.push(newPage); + setActivePage(newPage.id); + }) + ); + }; + + const deletePage = (pageIndex: number) => { + const newActivePageId = + pageIndex < document.pages.length - 1 + ? document.pages[pageIndex + 1].id // If it's not the last page, select the next one + : document.pages[pageIndex - 1].id; // Otherwise, select the previous one + + setDocument(lastDocument => + produce(lastDocument, draft => { + draft.pages = draft.pages.filter( + currentPage => document.pages[pageIndex].id !== currentPage.id + ); + }) + ); + + setActivePage(newActivePageId); + }; + + const setActivePage = (pageId: string) => { + selectionInfo.clearSelection(); + selectionInfo.shapeRefs.current = {}; + + setDocument(lastDocument => + produce(lastDocument, draft => { + const pageIndex = draft.pages.findIndex(page => page.id === pageId); + if (pageIndex !== -1) { + draft.activePageIndex = pageIndex; + } + }) + ); + }; + + const editPageTitle = (pageIndex: number, newName: string) => { + setDocument(lastDocument => + produce(lastDocument, draft => { + draft.pages[pageIndex].name = newName; + }) + ); + }; + const pasteShapes = (shapes: ShapeModel[]) => { const newShapes: ShapeModel[] = shapes.map(shape => { shape.id = uuidv4(); return shape; }); - setDocument(prevDocument => ({ - ...prevDocument, - shapes: [...prevDocument.shapes, ...newShapes], - })); + if (isPageIndexValid(document)) { + setDocument(lastDocument => + produce(lastDocument, draft => { + draft.pages[lastDocument.activePageIndex].shapes.push(...newShapes); + }) + ); + } // Just select the new pasted shapes // need to wait for the shapes to be rendered (previous set document is async) @@ -70,20 +150,28 @@ export const CanvasProvider: React.FC = props => { }; const { copyShapeToClipboard, pasteShapeFromClipboard, canCopy, canPaste } = - useClipboard(pasteShapes, document.shapes, selectionInfo); + useClipboard( + pasteShapes, + document.pages[document.activePageIndex].shapes, + selectionInfo + ); - const clearCanvas = () => { - setDocument({ shapes: [] }); + const createNewFullDocument = () => { + setDocument(createDefaultDocumentModel()); }; const deleteSelectedShapes = () => { - setDocument(prevDocument => ({ - ...prevDocument, - shapes: removeShapesFromList( - selectionInfo.selectedShapesIds, - prevDocument.shapes - ), - })); + if (isPageIndexValid(document)) { + setDocument(lastDocument => + produce(lastDocument, draft => { + draft.pages[lastDocument.activePageIndex].shapes = + removeShapesFromList( + selectionInfo.selectedShapesIds, + draft.pages[lastDocument.activePageIndex].shapes + ); + }) + ); + } }; // TODO: instenad of x,y use Coord and reduce the number of arguments @@ -93,14 +181,17 @@ export const CanvasProvider: React.FC = props => { y: number, otherProps?: OtherProps ) => { + if (!isPageIndexValid(document)) { + return ''; + } + const newShape = createShape({ x, y }, type, otherProps); - setDocument(({ shapes }) => { - const newShapes = [...shapes, newShape]; - return { - shapes: newShapes, - }; - }); + setDocument(lastDocument => + produce(lastDocument, draft => { + draft.pages[lastDocument.activePageIndex].shapes.push(newShape); + }) + ); return newShape.id; }; @@ -111,27 +202,43 @@ export const CanvasProvider: React.FC = props => { size: Size, skipHistory: boolean = false ) => { + if (!isPageIndexValid(document)) { + return; + } + if (skipHistory) { - setShapesSkipHistory(({ shapes }) => ({ - shapes: shapes.map(shape => - shape.id === id ? { ...shape, ...position, ...size } : shape - ), - })); + setShapesSkipHistory(fullDocument => { + return produce(fullDocument, draft => { + draft.pages[document.activePageIndex].shapes = draft.pages[ + document.activePageIndex + ].shapes.map(shape => + shape.id === id ? { ...shape, ...position, ...size } : shape + ); + }); + }); } else { - setDocument(({ shapes }) => ({ - shapes: shapes.map(shape => - shape.id === id ? { ...shape, ...position, ...size } : shape - ), - })); + setDocument(fullDocument => { + return produce(fullDocument, draft => { + draft.pages[document.activePageIndex].shapes = draft.pages[ + document.activePageIndex + ].shapes.map(shape => + shape.id === id ? { ...shape, ...position, ...size } : shape + ); + }); + }); } }; const updateShapePosition = (id: string, { x, y }: Coord) => { - setDocument(({ shapes }) => ({ - shapes: shapes.map(shape => - shape.id === id ? { ...shape, x, y } : shape - ), - })); + if (isPageIndexValid(document)) { + setDocument(fullDocument => { + return produce(fullDocument, draft => { + draft.pages[document.activePageIndex].shapes = draft.pages[ + document.activePageIndex + ].shapes.map(shape => (shape.id === id ? { ...shape, x, y } : shape)); + }); + }); + } }; const doUndo = () => { @@ -163,10 +270,10 @@ export const CanvasProvider: React.FC = props => { return ( = props => { loadDocument, isInlineEditing, setIsInlineEditing, + fileName, + setFileName, + fullDocument: document, + addNewPage, + duplicatePage, + setActivePage, + deletePage, + editPageTitle, + isThumbnailContextMenuVisible, + setIsThumbnailContextMenuVisible, }} > {children} diff --git a/src/core/providers/canvas/use-selection.hook.ts b/src/core/providers/canvas/use-selection.hook.ts index e8c491c3..e5ca7ac9 100644 --- a/src/core/providers/canvas/use-selection.hook.ts +++ b/src/core/providers/canvas/use-selection.hook.ts @@ -3,6 +3,8 @@ import Konva from 'konva'; import { OtherProps, ShapeModel, ShapeRefs, ShapeType } from '@/core/model'; import { DocumentModel, SelectionInfo, ZIndexAction } from './canvas.model'; import { performZIndexAction } from './zindex.util'; +import { getActivePageShapes, isPageIndexValid } from './canvas.business'; +import { produce } from 'immer'; export const useSelection = ( document: DocumentModel, @@ -28,7 +30,11 @@ export const useSelection = ( // Remove unused shapes and reset selectedShapeId if it no longer exists useEffect(() => { - const shapes = document.shapes; + if (!isPageIndexValid(document)) { + return; + } + + const shapes = getActivePageShapes(document); const currentIds = shapes.map(shape => shape.id); // 1. First cleanup Refs, let's get the list of shape and if there are any @@ -50,7 +56,7 @@ export const useSelection = ( setSelectedShapeType(null); } } - }, [document.shapes, selectedShapesIds]); + }, [document.pages, selectedShapesIds]); const isDeselectSingleItem = (arrayIds: string[]) => { return ( @@ -81,11 +87,17 @@ export const useSelection = ( type: ShapeType, isUserDoingMultipleSelection: boolean ) => { + // When chaging active pages, the refs are not yet updated + // check if this is something temporary or final solution + if (Object.keys(shapeRefs.current).length === 0) { + return; + } + // I want to know if the ids is string or array const arrayIds = typeof ids === 'string' ? [ids] : ids; if (!isUserDoingMultipleSelection) { - // No multiple selectio, just replace selection with current selected item(s) + // No multiple selection, just replace selection with current selected item(s) selectedShapesRefs.current = arrayIds.map( id => shapeRefs.current[id].current ); @@ -113,41 +125,55 @@ export const useSelection = ( setSelectedShapeType(type); }; + const clearSelection = () => { + transformerRef.current?.nodes([]); + selectedShapesRefs.current = []; + setSelectedShapesIds([]); + setSelectedShapeType(null); + }; + const handleClearSelection = ( mouseEvent?: | Konva.KonvaEventObject | Konva.KonvaEventObject ) => { if (!mouseEvent || mouseEvent.target === mouseEvent.target.getStage()) { - transformerRef.current?.nodes([]); - selectedShapesRefs.current = []; - setSelectedShapesIds([]); - setSelectedShapeType(null); + clearSelection(); } }; const setZIndexOnSelected = (action: ZIndexAction) => { - setDocument(prevDocument => ({ - shapes: performZIndexAction( - selectedShapesIds, - action, - prevDocument.shapes - ), - })); + if (!isPageIndexValid(document)) return; + + setDocument(prevDocument => + produce(prevDocument, draft => { + draft.pages[prevDocument.activePageIndex].shapes = performZIndexAction( + selectedShapesIds, + action, + getActivePageShapes(prevDocument) + ); + }) + ); }; const updateTextOnSelected = (text: string) => { + if (!isPageIndexValid(document)) return; + // Only when selection is one if (selectedShapesIds.length !== 1) { return; } const selectedShapeId = selectedShapesIds[0]; - setDocument(prevDocument => ({ - shapes: prevDocument.shapes.map(shape => - shape.id === selectedShapeId ? { ...shape, text } : shape - ), - })); + setDocument(prevDocument => + produce(prevDocument, draft => { + draft.pages[prevDocument.activePageIndex].shapes = draft.pages[ + prevDocument.activePageIndex + ].shapes.map(shape => + shape.id === selectedShapeId ? { ...shape, text } : shape + ); + }) + ); }; // TODO: Rather implement this using immmer @@ -156,6 +182,8 @@ export const useSelection = ( key: K, value: OtherProps[K] ) => { + if (!isPageIndexValid(document)) return; + // TODO: Right now applying this only to single selection // in the future we could apply to all selected shapes // BUT, we have to show only common shapes (pain in the neck) @@ -165,13 +193,18 @@ export const useSelection = ( } const selectedShapeId = selectedShapesIds[0]; - setDocument(prevDocument => ({ - shapes: prevDocument.shapes.map(shape => - shape.id === selectedShapeId - ? { ...shape, otherProps: { ...shape.otherProps, [key]: value } } - : shape - ), - })); + + setDocument(prevDocument => + produce(prevDocument, draft => { + draft.pages[prevDocument.activePageIndex].shapes = draft.pages[ + prevDocument.activePageIndex + ].shapes.map(shape => + shape.id === selectedShapeId + ? { ...shape, otherProps: { ...shape.otherProps, [key]: value } } + : shape + ); + }) + ); }; // Added index, right now we got multiple selection @@ -187,7 +220,9 @@ export const useSelection = ( const selectedShapeId = selectedShapesIds[index]; - return document.shapes.find(shape => shape.id === selectedShapeId); + return getActivePageShapes(document).find( + shape => shape.id === selectedShapeId + ); }; return { @@ -195,6 +230,7 @@ export const useSelection = ( shapeRefs, handleSelected, handleClearSelection, + clearSelection, selectedShapesRefs, selectedShapesIds, selectedShapeType, diff --git a/src/pods/canvas/canvas.pod.tsx b/src/pods/canvas/canvas.pod.tsx index 9363dd00..930989ff 100644 --- a/src/pods/canvas/canvas.pod.tsx +++ b/src/pods/canvas/canvas.pod.tsx @@ -132,6 +132,7 @@ export const CanvasPod = () => { onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} + id="konva-stage" // data-id did not work for some reason > { diff --git a/src/pods/canvas/model/inline-editable.model.ts b/src/pods/canvas/model/inline-editable.model.ts index 7db53fcf..53360f45 100644 --- a/src/pods/canvas/model/inline-editable.model.ts +++ b/src/pods/canvas/model/inline-editable.model.ts @@ -31,6 +31,9 @@ const inlineEditableShapes = new Set([ 'buttonBar', 'tabsBar', 'tooltip', + 'timepickerinput', + 'datepickerinput', + 'browser', ]); // Check if a shape type allows inline editing @@ -66,6 +69,9 @@ const shapeTypesWithDefaultText = new Set([ 'buttonBar', 'tabsBar', 'link', + 'timepickerinput', + 'datepickerinput', + 'browser', ]); // Map of ShapeTypes to their default text values @@ -98,6 +104,9 @@ const defaultTextValueMap: Partial> = { buttonBar: 'Button 1, Button 2, Button 3', tabsBar: 'Tab 1, Tab 2, Tab 3', link: 'link', + timepickerinput: 'hh:mm', + datepickerinput: new Date().toLocaleDateString(), + browser: 'https://example.com', }; export const generateDefaultTextValue = ( diff --git a/src/pods/canvas/model/shape-other-props.utils.ts b/src/pods/canvas/model/shape-other-props.utils.ts index 6cae99e5..ad713a11 100644 --- a/src/pods/canvas/model/shape-other-props.utils.ts +++ b/src/pods/canvas/model/shape-other-props.utils.ts @@ -25,6 +25,13 @@ export const generateDefaultOtherProps = ( }; case 'button': case 'textarea': + return { + stroke: BASIC_SHAPE.DEFAULT_STROKE_COLOR, + backgroundColor: BASIC_SHAPE.DEFAULT_FILL_BACKGROUND, + textColor: BASIC_SHAPE.DEFAULT_FILL_TEXT, + strokeStyle: [], + borderRadius: `${BASIC_SHAPE.DEFAULT_CORNER_RADIUS}`, + }; case 'vertical-menu': case 'horizontal-menu': return { @@ -99,7 +106,12 @@ export const generateDefaultOtherProps = ( strokeStyle: [], borderRadius: `${INPUT_SHAPE.DEFAULT_CORNER_RADIUS}`, }; - case 'line': + case 'horizontalLine': + return { + stroke: '#000000', + strokeStyle: [], + }; + case 'verticalLine': return { stroke: '#000000', strokeStyle: [], @@ -178,6 +190,7 @@ export const generateDefaultOtherProps = ( categories: ['IT'], }, iconSize: 'M', + stroke: BASIC_SHAPE.DEFAULT_STROKE_COLOR, }; case 'image': return { diff --git a/src/pods/canvas/model/shape-size.mapper.ts b/src/pods/canvas/model/shape-size.mapper.ts index 6509a670..40bd9c5c 100644 --- a/src/pods/canvas/model/shape-size.mapper.ts +++ b/src/pods/canvas/model/shape-size.mapper.ts @@ -33,7 +33,8 @@ import { getDiamondShapeSizeRestrictions, getImageShapeSizeRestrictions, getLargeArrowShapeSizeRestrictions, - getlineShapeRestrictions, + getHorizontalLineShapeRestrictions, + getVerticalLineShapeRestrictions, getPostItShapeSizeRestrictions, getRectangleShapeSizeRestrictions, getStarShapeSizeRestrictions, @@ -98,7 +99,8 @@ const shapeSizeMap: Record ShapeSizeRestrictions> = { rectangle: getRectangleShapeSizeRestrictions, videoPlayer: getVideoPlayerShapeSizeRestrictions, diamond: getDiamondShapeSizeRestrictions, - line: getlineShapeRestrictions, + horizontalLine: getHorizontalLineShapeRestrictions, + verticalLine: getVerticalLineShapeRestrictions, accordion: getAccordionShapeSizeRestrictions, triangle: getTriangleShapeSizeRestrictions, postit: getPostItShapeSizeRestrictions, diff --git a/src/pods/canvas/model/transformer.model.ts b/src/pods/canvas/model/transformer.model.ts index 4115e76a..12769dc7 100644 --- a/src/pods/canvas/model/transformer.model.ts +++ b/src/pods/canvas/model/transformer.model.ts @@ -45,14 +45,12 @@ export const generateTypeOfTransformer = (shapeType: ShapeType): string[] => { switch (shapeType) { case 'label': case 'input': - case 'button': case 'combobox': - case 'line': + case 'horizontalLine': case 'listbox': case 'checkbox': case 'toggleswitch': case 'progressbar': - case 'datepickerinput': case 'timepickerinput': case 'radiobutton': case 'horizontal-menu': @@ -68,6 +66,7 @@ export const generateTypeOfTransformer = (shapeType: ShapeType): string[] => { case 'buttonBar': case 'slider': return ['middle-left', 'middle-right']; + case 'verticalLine': case 'verticalScrollBar': return ['top-center', 'bottom-center']; case 'icon': diff --git a/src/pods/canvas/shape-renderer/index.tsx b/src/pods/canvas/shape-renderer/index.tsx index 8efb3f10..cff2f7a3 100644 --- a/src/pods/canvas/shape-renderer/index.tsx +++ b/src/pods/canvas/shape-renderer/index.tsx @@ -46,7 +46,8 @@ import { renderDiamond, renderTriangle, renderRectangle, - renderLine, + renderHorizontalLine, + renderVerticalLine, renderCircle, renderStar, renderPostit, @@ -120,8 +121,10 @@ export const renderShapeComponent = ( return renderLineChart(shape, shapeRenderedProps); case 'diamond': return renderDiamond(shape, shapeRenderedProps); - case 'line': - return renderLine(shape, shapeRenderedProps); + case 'horizontalLine': + return renderHorizontalLine(shape, shapeRenderedProps); + case 'verticalLine': + return renderVerticalLine(shape, shapeRenderedProps); case 'accordion': return renderAccordion(shape, shapeRenderedProps); case 'triangle': diff --git a/src/pods/canvas/shape-renderer/simple-basic-shapes/horizontal-line.renderer.tsx b/src/pods/canvas/shape-renderer/simple-basic-shapes/horizontal-line.renderer.tsx new file mode 100644 index 00000000..08403bfc --- /dev/null +++ b/src/pods/canvas/shape-renderer/simple-basic-shapes/horizontal-line.renderer.tsx @@ -0,0 +1,31 @@ +import { HorizontalLineShape } from '@/common/components/mock-components/front-basic-shapes'; +import { ShapeRendererProps } from '../model'; +import { ShapeModel } from '@/core/model'; + +export const renderHorizontalLine = ( + shape: ShapeModel, + shapeRenderedProps: ShapeRendererProps +) => { + const { handleSelected, shapeRefs, handleDragEnd, handleTransform } = + shapeRenderedProps; + + return ( + + ); +}; diff --git a/src/pods/canvas/shape-renderer/simple-basic-shapes/index.ts b/src/pods/canvas/shape-renderer/simple-basic-shapes/index.ts index 75daef9f..a676e622 100644 --- a/src/pods/canvas/shape-renderer/simple-basic-shapes/index.ts +++ b/src/pods/canvas/shape-renderer/simple-basic-shapes/index.ts @@ -2,7 +2,8 @@ export * from './rectangle.rerender'; export * from './postit.rerender'; export * from './diamond.renderer'; export * from './triangle.renderer'; -export * from './line.renderer'; +export * from './horizontal-line.renderer'; +export * from './vertical-line.renderer'; export * from './circle.renderer'; export * from './star.renderer'; export * from './large-arrow.renderer'; diff --git a/src/pods/canvas/shape-renderer/simple-basic-shapes/line.renderer.tsx b/src/pods/canvas/shape-renderer/simple-basic-shapes/vertical-line.renderer.tsx similarity index 82% rename from src/pods/canvas/shape-renderer/simple-basic-shapes/line.renderer.tsx rename to src/pods/canvas/shape-renderer/simple-basic-shapes/vertical-line.renderer.tsx index 2ce67348..fcfde41f 100644 --- a/src/pods/canvas/shape-renderer/simple-basic-shapes/line.renderer.tsx +++ b/src/pods/canvas/shape-renderer/simple-basic-shapes/vertical-line.renderer.tsx @@ -1,8 +1,8 @@ -import { LineShape } from '@/common/components/mock-components/front-basic-shapes'; +import { VerticalLineShape } from '@/common/components/mock-components/front-basic-shapes'; import { ShapeRendererProps } from '../model'; import { ShapeModel } from '@/core/model'; -export const renderLine = ( +export const renderVerticalLine = ( shape: ShapeModel, shapeRenderedProps: ShapeRendererProps ) => { @@ -10,7 +10,7 @@ export const renderLine = ( shapeRenderedProps; return ( - ); }; diff --git a/src/pods/canvas/shape-renderer/simple-component/icon.renderer.tsx b/src/pods/canvas/shape-renderer/simple-component/icon.renderer.tsx index 702c3ecb..6cb0573b 100644 --- a/src/pods/canvas/shape-renderer/simple-component/icon.renderer.tsx +++ b/src/pods/canvas/shape-renderer/simple-component/icon.renderer.tsx @@ -27,6 +27,7 @@ export const renderIcon = ( onTransformEnd={handleTransform} iconInfo={shape.otherProps?.icon} iconSize={shape.otherProps?.iconSize} + stroke={shape.otherProps?.stroke} /> ); }; diff --git a/src/pods/canvas/shape-renderer/simple-component/timepickerinput.renderer.tsx b/src/pods/canvas/shape-renderer/simple-component/timepickerinput.renderer.tsx index 99bc8425..6476b6b6 100644 --- a/src/pods/canvas/shape-renderer/simple-component/timepickerinput.renderer.tsx +++ b/src/pods/canvas/shape-renderer/simple-component/timepickerinput.renderer.tsx @@ -25,6 +25,8 @@ export const renderTimepickerinput = ( onDragEnd={handleDragEnd(shape.id)} onTransform={handleTransform} onTransformEnd={handleTransform} + isEditable={shape.allowsInlineEdition} + text={shape.text} otherProps={shape.otherProps} /> ); diff --git a/src/pods/canvas/shape-renderer/simple-container/browserwindow.renderer.tsx b/src/pods/canvas/shape-renderer/simple-container/browserwindow.renderer.tsx index ccfd8809..aadcf152 100644 --- a/src/pods/canvas/shape-renderer/simple-container/browserwindow.renderer.tsx +++ b/src/pods/canvas/shape-renderer/simple-container/browserwindow.renderer.tsx @@ -24,6 +24,8 @@ export const renderBrowserWindow = ( onDragEnd={handleDragEnd(shape.id)} onTransform={handleTransform} onTransformEnd={handleTransform} + isEditable={shape.allowsInlineEdition} + text={shape.text} /> ); }; diff --git a/src/pods/canvas/use-multiple-selection-shape.hook.tsx b/src/pods/canvas/use-multiple-selection-shape.hook.tsx index 3e4b745a..698c265c 100644 --- a/src/pods/canvas/use-multiple-selection-shape.hook.tsx +++ b/src/pods/canvas/use-multiple-selection-shape.hook.tsx @@ -4,6 +4,7 @@ import Konva from 'konva'; import { useState } from 'react'; import { SelectionRect } from './model'; import { + areCoordsInsideRect, findFirstShapeInCoords, getSelectedShapesFromSelectionRect, } from './use-multiple-selection.business'; @@ -11,6 +12,8 @@ import { getTransformerBoxAndCoords } from './transformer.utils'; import { calculateScaledCoordsFromCanvasDivCoordinatesNoScroll } from './canvas.util'; import { Stage } from 'konva/lib/Stage'; import { isUserDoingMultipleSelectionUsingCtrlOrCmdKey } from '@/common/utils/shapes'; +import { KonvaEventObject } from 'konva/lib/Node'; +import { useCanvasContext } from '@/core/providers'; // There's a bug here: if you make a multiple selectin and start dragging // inside the selection but on a blank area it won't drag the selection @@ -47,6 +50,8 @@ export const useMultipleSelectionShapeHook = ( visible: false, }); + const { setIsThumbnailContextMenuVisible } = useCanvasContext(); + const isDraggingSelection = (mouseCoords: Coord) => { if (!transformerRef.current) { return false; @@ -82,9 +87,39 @@ export const useMultipleSelectionShapeHook = ( ); }; + const isUserClickingOnTransformer = ( + e: KonvaEventObject | KonvaEventObject + ) => { + const transformerRect = transformerRef.current?.getClientRect(); + console.log(transformerRect); + const mousePosition = e.target?.getStage()?.getPointerPosition() ?? { + x: 0, + y: 0, + }; + + return ( + transformerRect && + areCoordsInsideRect( + mousePosition.x, + mousePosition.y, + transformerRect.x, + transformerRect.y, + transformerRect.width, + transformerRect.height + ) + ); + }; + const handleMouseDown = ( e: Konva.KonvaEventObject | Konva.KonvaEventObject ) => { + // Edge case if you drag on one of the transformer rectangle + // resize small boxes but you are on the outer part of the shape + // it will first select the shape that is behind (e.g. a windows or phone container) + // then the transformer (odd behavior, see: ) + if (isUserClickingOnTransformer(e)) { + return; + } // If user is holding ctrl or cmd key let's abort drag and drop multiple selection // He is trying to multiselect shape by using the keyboard if (isUserDoingMultipleSelectionUsingCtrlOrCmdKey(e)) { @@ -134,6 +169,8 @@ export const useMultipleSelectionShapeHook = ( height: 0, visible: true, }); + + setIsThumbnailContextMenuVisible(false); }; const handleMouseMove = (e: any) => { diff --git a/src/pods/canvas/use-multiple-selection.business.ts b/src/pods/canvas/use-multiple-selection.business.ts index 9a161cc6..f12be5eb 100644 --- a/src/pods/canvas/use-multiple-selection.business.ts +++ b/src/pods/canvas/use-multiple-selection.business.ts @@ -1,6 +1,22 @@ import { Coord, ShapeModel, ShapeRefs } from '@/core/model'; import { SelectionRect } from './model'; +export const areCoordsInsideRect = ( + pointX: number, + pointY: number, + rectX: number, + rectY: number, + rectWidth: number, + rectHeight: number +): boolean => { + return ( + pointX >= rectX && + pointX <= rectX + rectWidth && + pointY >= rectY && + pointY <= rectY + rectHeight + ); +}; + const isShapeInsideSelectionRect = ( shapeRect: SelectionRect, selectionRect: SelectionRect diff --git a/src/pods/canvas/use-transform.hook.ts b/src/pods/canvas/use-transform.hook.ts index ac812661..5ee9da9c 100644 --- a/src/pods/canvas/use-transform.hook.ts +++ b/src/pods/canvas/use-transform.hook.ts @@ -33,6 +33,7 @@ export const useTransform = ( transformerRef.current.enabledAnchors( selectedShape.attrs.typeOfTransformer ); + transformerRef.current.rotateEnabled(false); } }; diff --git a/src/pods/context-menu/use-context-menu.hook.tsx b/src/pods/context-menu/use-context-menu.hook.tsx index d2c53fbb..3b8e1177 100644 --- a/src/pods/context-menu/use-context-menu.hook.tsx +++ b/src/pods/context-menu/use-context-menu.hook.tsx @@ -26,7 +26,11 @@ export const ContextMenu: React.FC = ({ dropRef }) => { const handleRightClick = (event: MouseEvent) => { event.preventDefault(); - if (selectionInfo.getSelectedShapeData()) { + if ( + selectionInfo.getSelectedShapeData() && + stageRef.current && + stageRef.current.container().contains(event.target as Node) + ) { setShowContextMenu(true); setContextMenuPosition({ x: event.clientX, y: event.clientY }); } diff --git a/src/pods/galleries/basic-shapes-gallery/basic-gallery-data/index.ts b/src/pods/galleries/basic-shapes-gallery/basic-gallery-data/index.ts index 05a2e866..7a2991af 100644 --- a/src/pods/galleries/basic-shapes-gallery/basic-gallery-data/index.ts +++ b/src/pods/galleries/basic-shapes-gallery/basic-gallery-data/index.ts @@ -1,13 +1,14 @@ import { ItemInfo } from '@/common/components/gallery/components/model'; export const mockBasicShapesCollection: ItemInfo[] = [ - { thumbnailSrc: '/shapes/postit.svg', type: 'postit' }, - { thumbnailSrc: '/shapes/image.svg', type: 'image' }, - { thumbnailSrc: '/shapes/rectangle.svg', type: 'rectangle' }, - { thumbnailSrc: '/shapes/triangle.svg', type: 'triangle' }, { thumbnailSrc: '/shapes/circle.svg', type: 'circle' }, { thumbnailSrc: '/shapes/diamond.svg', type: 'diamond' }, - { thumbnailSrc: '/shapes/star.svg', type: 'star' }, - { thumbnailSrc: '/shapes/line.svg', type: 'line' }, + { thumbnailSrc: '/shapes/horizontalLine.svg', type: 'horizontalLine' }, + { thumbnailSrc: '/shapes/image.svg', type: 'image' }, { thumbnailSrc: '/shapes/largeArrow.svg', type: 'largeArrow' }, + { thumbnailSrc: '/shapes/postit.svg', type: 'postit' }, + { thumbnailSrc: '/shapes/rectangle.svg', type: 'rectangle' }, + { thumbnailSrc: '/shapes/star.svg', type: 'star' }, + { thumbnailSrc: '/shapes/triangle.svg', type: 'triangle' }, + { thumbnailSrc: '/shapes/verticalLine.svg', type: 'verticalLine' }, ]; diff --git a/src/pods/galleries/component-gallery/component-gallery-data/index.ts b/src/pods/galleries/component-gallery/component-gallery-data/index.ts index ec5f7b14..19bb8c29 100644 --- a/src/pods/galleries/component-gallery/component-gallery-data/index.ts +++ b/src/pods/galleries/component-gallery/component-gallery-data/index.ts @@ -1,24 +1,24 @@ import { ItemInfo } from '@/common/components/gallery/components/model'; export const mockWidgetCollection: ItemInfo[] = [ - { thumbnailSrc: '/widgets/icon.svg', type: 'icon' }, - { thumbnailSrc: '/widgets/label.svg', type: 'label' }, - { thumbnailSrc: '/widgets/input.svg', type: 'input' }, { thumbnailSrc: '/widgets/button.svg', type: 'button' }, - { thumbnailSrc: '/widgets/textarea.svg', type: 'textarea' }, - { thumbnailSrc: '/widgets/combobox.svg', type: 'combobox' }, - { thumbnailSrc: '/widgets/radiobutton.svg', type: 'radiobutton' }, { thumbnailSrc: '/widgets/checkbox.svg', type: 'checkbox' }, - { thumbnailSrc: '/widgets/toggleswitch.svg', type: 'toggleswitch' }, - { thumbnailSrc: '/widgets/progressbar.svg', type: 'progressbar' }, - { thumbnailSrc: '/widgets/listbox.svg', type: 'listbox' }, - { thumbnailSrc: '/widgets/slider.svg', type: 'slider' }, + { thumbnailSrc: '/widgets/combobox.svg', type: 'combobox' }, { thumbnailSrc: '/widgets/datepicker.svg', type: 'datepickerinput' }, - { thumbnailSrc: '/widgets/timepicker.svg', type: 'timepickerinput' }, - { thumbnailSrc: '/widgets/tooltip.svg', type: 'tooltip' }, - { thumbnailSrc: '/widgets/verticalscrollbar.svg', type: 'verticalScrollBar' }, { thumbnailSrc: '/widgets/horizontalscrollbar.svg', type: 'horizontalScrollBar', }, + { thumbnailSrc: '/widgets/icon.svg', type: 'icon' }, + { thumbnailSrc: '/widgets/input.svg', type: 'input' }, + { thumbnailSrc: '/widgets/label.svg', type: 'label' }, + { thumbnailSrc: '/widgets/listbox.svg', type: 'listbox' }, + { thumbnailSrc: '/widgets/progressbar.svg', type: 'progressbar' }, + { thumbnailSrc: '/widgets/radiobutton.svg', type: 'radiobutton' }, + { thumbnailSrc: '/widgets/slider.svg', type: 'slider' }, + { thumbnailSrc: '/widgets/textarea.svg', type: 'textarea' }, + { thumbnailSrc: '/widgets/timepicker.svg', type: 'timepickerinput' }, + { thumbnailSrc: '/widgets/toggleswitch.svg', type: 'toggleswitch' }, + { thumbnailSrc: '/widgets/tooltip.svg', type: 'tooltip' }, + { thumbnailSrc: '/widgets/verticalscrollbar.svg', type: 'verticalScrollBar' }, ]; diff --git a/src/pods/galleries/container-gallery/container-gallery-data/index.ts b/src/pods/galleries/container-gallery/container-gallery-data/index.ts index 0def6bb8..6cb3a0d8 100644 --- a/src/pods/galleries/container-gallery/container-gallery-data/index.ts +++ b/src/pods/galleries/container-gallery/container-gallery-data/index.ts @@ -2,7 +2,7 @@ import { ItemInfo } from '@/common/components/gallery/components/model'; export const mockContainerCollection: ItemInfo[] = [ { thumbnailSrc: '/containers/browser.svg', type: 'browser' }, - { thumbnailSrc: '/containers/tablet.svg', type: 'tablet' }, { thumbnailSrc: 'containers/mobile.svg', type: 'mobilePhone' }, { thumbnailSrc: 'containers/modal-dialog.svg', type: 'modalDialog' }, + { thumbnailSrc: '/containers/tablet.svg', type: 'tablet' }, ]; diff --git a/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts b/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts index d2689139..983d76ae 100644 --- a/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts +++ b/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts @@ -1,26 +1,23 @@ import { ItemInfo } from '@/common/components/gallery/components/model'; export const mockRichComponentsCollection: ItemInfo[] = [ - { thumbnailSrc: '/rich-components/audioPlayer.svg', type: 'audioPlayer' }, - { thumbnailSrc: '/rich-components/table.svg', type: 'table' }, { thumbnailSrc: '/rich-components/accordion.svg', type: 'accordion' }, + { thumbnailSrc: '/rich-components/appBar.svg', type: 'appBar' }, + { thumbnailSrc: '/rich-components/audioPlayer.svg', type: 'audioPlayer' }, + { thumbnailSrc: '/rich-components/barchart.svg', type: 'bar' }, + { thumbnailSrc: '/rich-components/breadcrumb.svg', type: 'breadcrumb' }, + { thumbnailSrc: '/rich-components/button-bar-group.svg', type: 'buttonBar' }, + { thumbnailSrc: '/rich-components/calendar.svg', type: 'calendar' }, { thumbnailSrc: '/rich-components/horizontal-menu.svg', type: 'horizontal-menu', }, - { thumbnailSrc: '/rich-components/button-bar-group.svg', type: 'buttonBar' }, - { - thumbnailSrc: '/rich-components/vertical-menu.svg', - type: 'vertical-menu', - }, - { thumbnailSrc: '/rich-components/appBar.svg', type: 'appBar' }, - { thumbnailSrc: '/rich-components/breadcrumb.svg', type: 'breadcrumb' }, + { thumbnailSrc: '/rich-components/line-chart.svg', type: 'linechart' }, + { thumbnailSrc: '/rich-components/map.svg', type: 'map' }, { thumbnailSrc: '/rich-components/modal.svg', type: 'modal' }, + { thumbnailSrc: '/rich-components/pie.svg', type: 'pie' }, + { thumbnailSrc: '/rich-components/table.svg', type: 'table' }, { thumbnailSrc: '/rich-components/tabsbar.svg', type: 'tabsBar' }, - { thumbnailSrc: '/rich-components/calendar.svg', type: 'calendar' }, + { thumbnailSrc: '/rich-components/vertical-menu.svg', type: 'vertical-menu' }, { thumbnailSrc: '/rich-components/videoPlayer.svg', type: 'videoPlayer' }, - { thumbnailSrc: '/rich-components/pie.svg', type: 'pie' }, - { thumbnailSrc: '/rich-components/line-chart.svg', type: 'linechart' }, - { thumbnailSrc: '/rich-components/barchart.svg', type: 'bar' }, - { thumbnailSrc: '/rich-components/map.svg', type: 'map' }, ]; diff --git a/src/pods/properties/components/active-element-selector/active-element-selector.component.tsx b/src/pods/properties/components/active-element-selector/active-element-selector.component.tsx index dd827294..9b0c6b71 100644 --- a/src/pods/properties/components/active-element-selector/active-element-selector.component.tsx +++ b/src/pods/properties/components/active-element-selector/active-element-selector.component.tsx @@ -27,7 +27,7 @@ export const ActiveElementSelector: React.FC = ({ // Checking whether the type is tabsBar and parsing the text const isElementTypeSupported = - type === 'tabsBar' || 'buttonBar' || 'horizontal-menu'; + type === 'tabsBar' || 'buttonBar' || 'horizontal-menu' || 'timepickerinput'; const elementNames = isElementTypeSupported && text ? extractElementNames(text) : []; diff --git a/src/pods/properties/components/font-style/font-style.module.css b/src/pods/properties/components/font-style/font-style.module.css index 4863b13c..52b4fe3c 100644 --- a/src/pods/properties/components/font-style/font-style.module.css +++ b/src/pods/properties/components/font-style/font-style.module.css @@ -6,35 +6,12 @@ border-bottom: 1px solid var(--primary-300); } -.buttonsContainer { - display: flex; - gap: 1em; - align-items: center; - margin-left: auto; +.container :first-child { + flex: 1; } -.button { - border: none; - color: var(--text-color); - background-color: inherit; - width: auto; - min-width: 30px; - border-radius: 10px; - font-size: var(--fs-xs); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s ease-in-out; +.checkbox { + width: var(--space-md); + height: var(--space-md); cursor: pointer; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); -} - -.button:hover { - background-color: var(--primary-100); -} - -.active { - background-color: var(--primary-200); - box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2); - transform: translateY(2px); } diff --git a/src/pods/properties/components/font-style/font-style.tsx b/src/pods/properties/components/font-style/font-style.tsx index 3caf0d21..9667a98c 100644 --- a/src/pods/properties/components/font-style/font-style.tsx +++ b/src/pods/properties/components/font-style/font-style.tsx @@ -20,31 +20,15 @@ export const FontStyle: React.FC = props => { return (

{label}

-
- -
+
); }; diff --git a/src/pods/properties/components/font-variant/font-variant.module.css b/src/pods/properties/components/font-variant/font-variant.module.css index 4863b13c..52b4fe3c 100644 --- a/src/pods/properties/components/font-variant/font-variant.module.css +++ b/src/pods/properties/components/font-variant/font-variant.module.css @@ -6,35 +6,12 @@ border-bottom: 1px solid var(--primary-300); } -.buttonsContainer { - display: flex; - gap: 1em; - align-items: center; - margin-left: auto; +.container :first-child { + flex: 1; } -.button { - border: none; - color: var(--text-color); - background-color: inherit; - width: auto; - min-width: 30px; - border-radius: 10px; - font-size: var(--fs-xs); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s ease-in-out; +.checkbox { + width: var(--space-md); + height: var(--space-md); cursor: pointer; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); -} - -.button:hover { - background-color: var(--primary-100); -} - -.active { - background-color: var(--primary-200); - box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2); - transform: translateY(2px); } diff --git a/src/pods/properties/components/font-variant/font-variant.tsx b/src/pods/properties/components/font-variant/font-variant.tsx index f27aa293..dc995295 100644 --- a/src/pods/properties/components/font-variant/font-variant.tsx +++ b/src/pods/properties/components/font-variant/font-variant.tsx @@ -22,31 +22,15 @@ export const FontVariant: React.FC = props => { return (

{label}

-
- -
+
); }; diff --git a/src/pods/properties/components/icon-selector/modal/icons.ts b/src/pods/properties/components/icon-selector/modal/icons.ts index 77f75426..8646e562 100644 --- a/src/pods/properties/components/icon-selector/modal/icons.ts +++ b/src/pods/properties/components/icon-selector/modal/icons.ts @@ -578,25 +578,13 @@ export const iconCollection: IconInfo[] = [ 'phone', 'smartphone', 'tablet', - 'application', - 'play store', - 'google play', ], categories: ['IT'], }, { name: 'Apple', filename: 'apple.svg', - searchTerms: [ - 'apple', - 'system', - 'mobile', - 'phone', - 'smartphone', - 'tablet', - 'application', - 'app store', - ], + searchTerms: ['apple', 'system', 'mobile', 'phone', 'smartphone', 'tablet'], categories: ['IT'], }, { @@ -1187,7 +1175,7 @@ export const iconCollection: IconInfo[] = [ { name: 'Chat', filename: 'chat.svg', - searchTerms: ['chat', 'message', 'conversation', 'chatting'], + searchTerms: ['chat', 'message', 'conversation', 'chatting', 'comment'], categories: ['IT'], }, { @@ -1322,4 +1310,581 @@ export const iconCollection: IconInfo[] = [ searchTerms: ['pencil', 'edit', 'editing', 'edition'], categories: ['IT'], }, + { + name: 'Calendar', + filename: 'calendar.svg', + searchTerms: ['calendar', 'days', 'date', 'hours', 'time', 'schedule'], + categories: ['IT'], + }, + { + name: 'Cursor', + filename: 'cursor.svg', + searchTerms: ['cursor', 'pointer', 'select', 'mouse'], + categories: ['IT'], + }, + { + name: 'Cursor click', + filename: 'cursorclick.svg', + searchTerms: ['cursor', 'click', 'pointer', 'select', 'mouse'], + categories: ['IT'], + }, + { + name: 'Microphone', + filename: 'microphone.svg', + searchTerms: ['microphone', 'record', 'micro', 'mic'], + categories: ['IT'], + }, + { + name: 'Microphone off', + filename: 'microphoneslash.svg', + searchTerms: ['microphone', 'mute', 'silence', 'mic'], + categories: ['IT'], + }, + { + name: 'Webcam', + filename: 'webcam.svg', + searchTerms: ['webcam', 'camera', 'video', 'camcorder'], + categories: ['IT'], + }, + { + name: 'Webcam off', + filename: 'webcamslash.svg', + searchTerms: ['webcam', 'camera', 'slash', 'camcorder', 'off'], + categories: ['IT'], + }, + { + name: 'Empty Battery', + filename: 'emptybattery.svg', + searchTerms: ['battery', 'empty', 'discharged', 'energy'], + categories: ['IT'], + }, + { + name: 'Sign in', + filename: 'signin.svg', + searchTerms: ['sign in', 'login', 'enter', 'access'], + categories: ['IT'], + }, + { + name: 'Sign out', + filename: 'signout.svg', + searchTerms: ['sign out', 'logout', 'exit', 'leave'], + categories: ['IT'], + }, + { + name: 'Arrow Bend left', + filename: 'arrowbendupleft.svg', + searchTerms: ['arrow', 'left', 'bend', 'move'], + categories: ['IT'], + }, + { + name: 'Arrow Bend right', + filename: 'arrowbendupright.svg', + searchTerms: ['arrow', 'right', 'bend', 'move'], + categories: ['IT'], + }, + { + name: 'Dots Square', + filename: 'dotssquare.svg', + searchTerms: ['dots', 'square', 'menu', 'more'], + categories: ['IT'], + }, + { + name: 'Dots Vertical', + filename: 'dotsvertical.svg', + searchTerms: ['dots', 'vertical', 'menu', 'options'], + categories: ['IT'], + }, + { + name: 'Google Drive', + filename: 'drive.svg', + searchTerms: ['drive', 'google', 'cloud', 'storage'], + categories: ['IT'], + }, + { + name: 'Linux', + filename: 'linux.svg', + searchTerms: ['linux', 'system', 'software', 'desktop'], + categories: ['IT'], + }, + { + name: 'Windows', + filename: 'windows.svg', + searchTerms: ['windows', 'system', 'software', 'desktop'], + categories: ['IT'], + }, + { + name: 'Moon', + filename: 'moon.svg', + searchTerms: ['moon', 'night', 'dark', 'sky'], + categories: ['IT'], + }, + { + name: 'Alarm', + filename: 'alarm.svg', + searchTerms: ['alarm', 'clock', 'alert', 'ring'], + categories: ['IT'], + }, + { + name: 'Gitlab', + filename: 'gitlab.svg', + searchTerms: ['gitlab', 'code', 'repository', 'version control'], + categories: ['IT'], + }, + { + name: 'List dots', + filename: 'listdots.svg', + searchTerms: ['list', 'categorize', 'unordered', 'classify'], + categories: ['IT'], + }, + { + name: 'List checks', + filename: 'listchecks.svg', + searchTerms: ['list', 'categorize', 'unordered', 'classify'], + categories: ['IT'], + }, + { + name: 'List dashes', + filename: 'listdashes.svg', + searchTerms: ['list', 'categorize', 'unordered', 'classify'], + categories: ['IT'], + }, + { + name: 'List heart', + filename: 'listheart.svg', + searchTerms: ['list', 'categorize', 'unordered', 'classify'], + categories: ['IT'], + }, + { + name: 'Search list', + filename: 'searchlist.svg', + searchTerms: ['search', 'list', 'find', 'lookup', 'query'], + categories: ['IT'], + }, + { + name: 'List numbers', + filename: 'listnumbers.svg', + searchTerms: ['list', 'categorize', 'ordered', 'classify', 'numbers'], + categories: ['IT'], + }, + { + name: 'Add list', + filename: 'addlist.svg', + searchTerms: ['add', 'list', 'categorize', 'unordered', 'classify'], + categories: ['IT'], + }, + { + name: 'list star', + filename: 'liststar.svg', + searchTerms: ['list', 'categorize', 'unordered', 'classify', 'favorite'], + categories: ['IT'], + }, + { + name: 'Help', + filename: 'help.svg', + searchTerms: ['help', 'question', 'support', 'assist'], + categories: ['IT'], + }, + { + name: 'Star', + filename: 'star.svg', + searchTerms: ['star', 'favorite', 'like', 'rate'], + categories: ['IT'], + }, + { + name: 'Text align center', + filename: 'textaligncenter.svg', + searchTerms: ['text', 'align', 'center', 'format', 'paragraph'], + categories: ['IT'], + }, + { + name: 'Text align justify', + filename: 'textalignjustify.svg', + searchTerms: ['text', 'align', 'justify', 'format', 'paragraph'], + categories: ['IT'], + }, + { + name: 'Text align left', + filename: 'textalignleft.svg', + searchTerms: ['text', 'align', 'left', 'format', 'paragraph'], + categories: ['IT'], + }, + { + name: 'Text align right', + filename: 'textalignright.svg', + searchTerms: ['text', 'align', 'right', 'format', 'paragraph'], + categories: ['IT'], + }, + { + name: 'Text indent', + filename: 'textindent.svg', + searchTerms: ['text', 'indent', 'format', 'paragraph'], + categories: ['IT'], + }, + { + name: 'Upload', + filename: 'upload.svg', + searchTerms: ['upload', 'transfer', 'load', 'charge', 'import'], + categories: ['IT'], + }, + { + name: 'Warning circle', + filename: 'warningcircle.svg', + searchTerms: ['warning', 'circle', 'alert', 'caution'], + categories: ['IT'], + }, + { + name: 'Warning', + filename: 'warning.svg', + searchTerms: ['warning', 'alert', 'caution', 'attention'], + categories: ['IT'], + }, + { + name: 'Shield check', + filename: 'shieldcheck.svg', + searchTerms: ['shield', 'checked', 'security', 'verified'], + categories: ['IT'], + }, + { + name: 'Shield checkered', + filename: 'shieldcheckered.svg', + searchTerms: ['shield', 'protection', 'security', 'defense'], + categories: ['IT'], + }, + { + name: 'Shield desactivated', + filename: 'shieldslash.svg', + searchTerms: ['shield', 'desactivated', 'security', 'off'], + categories: ['IT'], + }, + { + name: 'Shield warning', + filename: 'shieldwarning.svg', + searchTerms: ['shield', 'warning', 'security', 'alert'], + categories: ['IT'], + }, + { + name: 'Shield', + filename: 'normalshield.svg', + searchTerms: ['shield', 'protection', 'security', 'defense'], + categories: ['IT'], + }, + { + name: 'Scissors', + filename: 'scissors.svg', + searchTerms: ['scissors', 'cut', 'tool', 'clip'], + categories: ['IT'], + }, + { + name: 'Phone', + filename: 'phone.svg', + searchTerms: ['phone', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Phone call', + filename: 'phonecall.svg', + searchTerms: ['phone', 'call', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Phone hang', + filename: 'phonehang.svg', + searchTerms: ['phone', 'hang', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Phone disconnected', + filename: 'phoneslash.svg', + searchTerms: ['phone', 'disconnected', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Phone pause', + filename: 'phonepause.svg', + searchTerms: ['phone', 'pause', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Call phone incoming', + filename: 'callphoneincoming.svg', + searchTerms: [ + 'call', + 'phone', + 'incoming', + 'landline', + 'device', + 'telephone', + ], + categories: ['IT'], + }, + { + name: 'Phone list', + filename: 'phonelist.svg', + searchTerms: ['phone', 'list', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Music note', + filename: 'musicnote.svg', + searchTerms: ['music', 'note', 'sound', 'audio', 'melody'], + categories: ['IT'], + }, + { + name: 'File document', + filename: 'filedoc.svg', + searchTerms: ['file', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'File pdf', + filename: 'filepdf.svg', + searchTerms: ['file', 'pdf', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'File png', + filename: 'filepng.svg', + searchTerms: ['file', 'png', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'File powerpoint', + filename: 'filepowerpoint.svg', + searchTerms: ['file', 'powerpoint', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'File jpg', + filename: 'filejpg.svg', + searchTerms: ['file', 'jpg', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'File excel', + filename: 'fileexcel.svg', + searchTerms: ['file', 'excel', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'Arrows clockwise', + filename: 'arrowsclockwise.svg', + searchTerms: ['arrows', 'clockwise', 'direction', 'rotate'], + categories: ['IT'], + }, + { + name: 'Arrows counter clockwise', + filename: 'arrowscounterclockwise.svg', + searchTerms: ['arrows', 'counter clockwise', 'direction', 'rotate'], + categories: ['IT'], + }, + { + name: 'Arrow fat down', + filename: 'arrowfatdown.svg', + searchTerms: ['arrow', 'fat', 'down', 'move'], + categories: ['IT'], + }, + { + name: 'Arrow fat left', + filename: 'arrowfatleft.svg', + searchTerms: ['arrow', 'fat', 'left', 'move'], + categories: ['IT'], + }, + { + name: 'Arrow fat right', + filename: 'arrowfatright.svg', + searchTerms: ['arrow', 'fat', 'right', 'move'], + categories: ['IT'], + }, + { + name: 'Arrow fat up', + filename: 'arrowfatup.svg', + searchTerms: ['arrow', 'fat', 'up', 'move'], + categories: ['IT'], + }, + { + name: 'Check fat', + filename: 'checkfat.svg', + searchTerms: ['check', 'confirmation', 'validate', 'success'], + categories: ['IT'], + }, + { + name: 'Check', + filename: 'check.svg', + searchTerms: ['check', 'confirmation', 'validate', 'success'], + categories: ['IT'], + }, + { + name: 'Double check', + filename: 'doublecheck.svg', + searchTerms: ['check', 'confirmation', 'validate', 'double'], + categories: ['IT'], + }, + { + name: 'Start', + filename: 'home.svg', + searchTerms: ['start', 'home', 'begin', 'launch'], + categories: ['IT'], + }, + { + name: 'Company', + filename: 'company.svg', + searchTerms: ['company', 'business', 'enterprise', 'corporation'], + categories: ['IT'], + }, + { + name: 'Factory', + filename: 'factory.svg', + searchTerms: ['factory', 'industry', 'manufacture', 'production'], + categories: ['IT'], + }, + { + name: 'Keyboard', + filename: 'keyboard.svg', + searchTerms: ['keyboard', 'device', 'computer', 'write'], + categories: ['IT'], + }, + { + name: 'Printer', + filename: 'printer.svg', + searchTerms: ['printer', 'device', 'computer', 'imprint'], + categories: ['IT'], + }, + { + name: 'Plug', + filename: 'plug.svg', + searchTerms: ['plug', 'device', 'connect', 'power'], + categories: ['IT'], + }, + { + name: 'Copyright', + filename: 'copyright.svg', + searchTerms: [ + 'copyright', + 'rights', + 'protected', + 'intellectual', + 'property', + ], + categories: ['IT'], + }, + { + name: 'Caret down', + filename: 'caretdown.svg', + searchTerms: ['caret', 'down', 'arrow', 'move'], + categories: ['IT'], + }, + { + name: 'Caret left', + filename: 'caretleft.svg', + searchTerms: ['caret', 'left', 'arrow', 'move'], + categories: ['IT'], + }, + { + name: 'Caret up', + filename: 'caretup.svg', + searchTerms: ['caret', 'up', 'arrow', 'move'], + categories: ['IT'], + }, + { + name: 'Caret right', + filename: 'caretright.svg', + searchTerms: ['caret', 'right', 'arrow', 'move'], + categories: ['IT'], + }, + { + name: 'Camera', + filename: 'camera.svg', + searchTerms: ['camera', 'photo', 'shot', 'capture', 'snapshot'], + categories: ['IT'], + }, + { + name: 'Flag', + filename: 'flag.svg', + searchTerms: ['flag', 'signal', 'banderole', 'banner'], + categories: ['IT'], + }, + { + name: 'First aid', + filename: 'firstaid.svg', + searchTerms: ['hospital', 'medical', 'emergency', 'health'], + categories: ['IT'], + }, + { + name: 'Hammer', + filename: 'hammer.svg', + searchTerms: ['hammer', 'tool', 'build', 'repair'], + categories: ['IT'], + }, + { + name: 'Joystick', + filename: 'joystick.svg', + searchTerms: ['joystick', 'game', 'play', 'controller'], + categories: ['IT'], + }, + { + name: 'Controller', + filename: 'controller.svg', + searchTerms: ['controller', 'game', 'play', 'gamepad'], + categories: ['IT'], + }, + { + name: 'Key', + filename: 'key.svg', + searchTerms: ['key', 'secure', 'password', 'access'], + categories: ['IT'], + }, + { + name: 'Lock', + filename: 'lock.svg', + searchTerms: ['lock', 'secure', 'password', 'access'], + categories: ['IT'], + }, + { + name: 'Unlock', + filename: 'lockopen.svg', + searchTerms: ['unlock', 'open', 'access', 'password'], + categories: ['IT'], + }, + { + name: 'Flash', + filename: 'lightning.svg', + searchTerms: ['flash', 'lightning', 'energy', 'power'], + categories: ['IT'], + }, + { + name: 'Auto flash', + filename: 'autoflash.svg', + searchTerms: ['auto', 'flash', 'lightning', 'energy', 'power'], + categories: ['IT'], + }, + { + name: 'Flash slash', + filename: 'flashslash.svg', + searchTerms: ['flash', 'slash', 'lightning', 'energy', 'power'], + categories: ['IT'], + }, + { + name: 'Another Mouse', + filename: 'alternativemouse.svg', + searchTerms: ['mouse', 'device', 'computer', 'click'], + categories: ['IT'], + }, + { + name: 'Power', + filename: 'power.svg', + searchTerms: ['power', 'on', 'off', 'energy'], + categories: ['IT'], + }, + { + name: 'Spinner', + filename: 'spinner.svg', + searchTerms: ['spinner', 'loading', 'wait', 'progress'], + categories: ['IT'], + }, + { + name: 'Subtitles', + filename: 'subtitles.svg', + searchTerms: ['subtitles', 'caption', 'language', 'translate'], + categories: ['IT'], + }, ]; diff --git a/src/pods/properties/components/stroke-style/stroke.style.component.tsx b/src/pods/properties/components/stroke-style/stroke.style.component.tsx index e1af3ad6..4529bc4c 100644 --- a/src/pods/properties/components/stroke-style/stroke.style.component.tsx +++ b/src/pods/properties/components/stroke-style/stroke.style.component.tsx @@ -31,6 +31,7 @@ export const StrokeStyle: React.FC = props => { + ); diff --git a/src/pods/properties/components/text-decoration/text-decoration.module.css b/src/pods/properties/components/text-decoration/text-decoration.module.css index 4863b13c..52b4fe3c 100644 --- a/src/pods/properties/components/text-decoration/text-decoration.module.css +++ b/src/pods/properties/components/text-decoration/text-decoration.module.css @@ -6,35 +6,12 @@ border-bottom: 1px solid var(--primary-300); } -.buttonsContainer { - display: flex; - gap: 1em; - align-items: center; - margin-left: auto; +.container :first-child { + flex: 1; } -.button { - border: none; - color: var(--text-color); - background-color: inherit; - width: auto; - min-width: 30px; - border-radius: 10px; - font-size: var(--fs-xs); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s ease-in-out; +.checkbox { + width: var(--space-md); + height: var(--space-md); cursor: pointer; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); -} - -.button:hover { - background-color: var(--primary-100); -} - -.active { - background-color: var(--primary-200); - box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2); - transform: translateY(2px); } diff --git a/src/pods/properties/components/text-decoration/text-decoration.tsx b/src/pods/properties/components/text-decoration/text-decoration.tsx index d4fda69f..57dac3dd 100644 --- a/src/pods/properties/components/text-decoration/text-decoration.tsx +++ b/src/pods/properties/components/text-decoration/text-decoration.tsx @@ -22,34 +22,16 @@ export const TextDecoration: React.FC = props => { return (

{label}

-
- -
+
); }; diff --git a/src/pods/thumb-pages/components/context-menu/context-menu.component.module.css b/src/pods/thumb-pages/components/context-menu/context-menu.component.module.css new file mode 100644 index 00000000..82de8324 --- /dev/null +++ b/src/pods/thumb-pages/components/context-menu/context-menu.component.module.css @@ -0,0 +1,40 @@ +.context-menu { + position: absolute; + top: 50%; + left: 50%; + width: 80%; + height: auto; + transform: translate(-50%, -50%); + border: 1px solid var(--primary-500); + background-color: var(--primary-100); + opacity: 0.98; +} + +.container { + display: flex; + gap: 0.5em; + align-items: center; + font-size: var(--fs-xs); + padding: var(--space-xs) var(--space-md); + border-bottom: 1px solid var(--primary-300); + cursor: pointer; +} + +.container :first-child { + flex: 1; +} + +.container:hover { + background-color: var(--primary-200); +} + +.disabled { + cursor: not-allowed; + opacity: 0.5; + background-color: var(--primary-200); +} + +.shortcut { + color: var(--primary-400); + font-weight: 500; +} diff --git a/src/pods/thumb-pages/components/context-menu/context-menu.component.tsx b/src/pods/thumb-pages/components/context-menu/context-menu.component.tsx new file mode 100644 index 00000000..8b5a474e --- /dev/null +++ b/src/pods/thumb-pages/components/context-menu/context-menu.component.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { useCanvasContext } from '@/core/providers'; +import classes from './context-menu.component.module.css'; +import { CopyIcon, DeleteIcon, PencilIcon } from '@/common/components/icons'; + +interface ThumbPageContextMenuProps { + contextMenuRef: React.RefObject; + setShowContextMenu: (show: boolean) => void; + pageIndex: number; + setPageTitleBeingEdited: (index: number) => void; +} + +export const ThumbPageContextMenu: React.FunctionComponent< + ThumbPageContextMenuProps +> = props => { + const { + contextMenuRef, + setShowContextMenu, + pageIndex, + setPageTitleBeingEdited, + } = props; + const { + setIsThumbnailContextMenuVisible, + fullDocument, + duplicatePage, + deletePage, + } = useCanvasContext(); + + enum ContextButtonType { + 'Duplicate', + 'Rename', + 'Delete', + } + + const handleClickOnContextButton = ( + event: React.MouseEvent, + buttonClicked: ContextButtonType + ) => { + event.stopPropagation(); + switch (buttonClicked) { + case ContextButtonType.Duplicate: + duplicatePage(pageIndex); + break; + case ContextButtonType.Rename: + console.log('Rename'); + setPageTitleBeingEdited(pageIndex); + break; + case ContextButtonType.Delete: + if (fullDocument.pages.length !== 1) { + deletePage(pageIndex); + } + break; + } + setShowContextMenu(false); + setIsThumbnailContextMenuVisible(false); + }; + + return ( +
+
+ handleClickOnContextButton(event, ContextButtonType.Duplicate) + } + className={classes.container} + > +

Duplicate

+ +
+
+ handleClickOnContextButton(event, ContextButtonType.Rename) + } + className={classes.container} + > +

Rename

+ +
+
+ handleClickOnContextButton(event, ContextButtonType.Delete) + } + className={ + fullDocument.pages.length === 1 + ? `${classes.container} ${classes.disabled}` + : `${classes.container}` + } + > +

Delete

+ +
+
+ ); +}; diff --git a/src/pods/thumb-pages/components/context-menu/index.ts b/src/pods/thumb-pages/components/context-menu/index.ts new file mode 100644 index 00000000..61e476da --- /dev/null +++ b/src/pods/thumb-pages/components/context-menu/index.ts @@ -0,0 +1 @@ +export * from './context-menu.component'; diff --git a/src/pods/thumb-pages/components/index.ts b/src/pods/thumb-pages/components/index.ts new file mode 100644 index 00000000..f096ec4a --- /dev/null +++ b/src/pods/thumb-pages/components/index.ts @@ -0,0 +1,2 @@ +export * from './thumb-page'; +export * from './page-title-inline-edit.component'; diff --git a/src/pods/thumb-pages/components/page-title-inline-edit.component.tsx b/src/pods/thumb-pages/components/page-title-inline-edit.component.tsx new file mode 100644 index 00000000..da42d0fd --- /dev/null +++ b/src/pods/thumb-pages/components/page-title-inline-edit.component.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from 'react'; +import { useCanvasContext } from '@/core/providers'; + +interface PageTitleInlineEditProps { + pageIndex: number; + setPageTitleBeingEdited: (index: number | null) => void; +} + +export const PageTitleInlineEdit: React.FC = ({ + pageIndex, + setPageTitleBeingEdited, +}) => { + const { fullDocument, editPageTitle, setIsInlineEditing } = + useCanvasContext(); + const [inputValue, setInputValue] = useState( + fullDocument.pages[pageIndex].name + ); + const inputRef = React.useRef(null); + + const updatePageTitle = () => { + editPageTitle(pageIndex, inputValue); + setPageTitleBeingEdited(null); + setIsInlineEditing(false); + }; + + const handleAction = ( + event: React.FormEvent | React.FocusEvent + ) => { + if (event.type === 'submit') { + event.preventDefault(); + } + updatePageTitle(); + }; + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + setIsInlineEditing(true); + } + }, []); + + return ( +
+ setInputValue(e.target.value)} + onBlur={handleAction} + /> +
+ ); +}; diff --git a/src/pods/thumb-pages/components/thumb-page.module.css b/src/pods/thumb-pages/components/thumb-page.module.css new file mode 100644 index 00000000..bfa1c2be --- /dev/null +++ b/src/pods/thumb-pages/components/thumb-page.module.css @@ -0,0 +1,11 @@ +.container { + width: '200px'; + height: '180px'; + border: 3px solid; + border-color: black; + border-radius: 5px; + position: 'relative'; + background-color: white; + margin-top: 10px; + padding: 5px; +} diff --git a/src/pods/thumb-pages/components/thumb-page.tsx b/src/pods/thumb-pages/components/thumb-page.tsx new file mode 100644 index 00000000..3c031d0c --- /dev/null +++ b/src/pods/thumb-pages/components/thumb-page.tsx @@ -0,0 +1,75 @@ +import { ShapeRefs } from '@/core/model'; +import { useCanvasContext } from '@/core/providers'; +import { renderShapeComponent } from '@/pods/canvas/shape-renderer'; +import { calculateCanvasBounds } from '@/pods/toolbar/components/export-button/export-button.utils'; +import { KonvaEventObject } from 'konva/lib/Node'; +import { createRef, useRef } from 'react'; +import { Layer, Stage } from 'react-konva'; +import { ThumbPageContextMenu } from './context-menu'; +import { useContextMenu } from '../use-context-menu-thumb.hook'; +import classes from './thumb-page.module.css'; + +interface Props { + pageIndex: number; + onSetActivePage: (pageId: string) => void; + setPageTitleBeingEdited: (index: number) => void; +} + +export const ThumbPage: React.FunctionComponent = props => { + const { pageIndex, onSetActivePage, setPageTitleBeingEdited } = props; + const { fullDocument } = useCanvasContext(); + const page = fullDocument.pages[pageIndex]; + const shapes = page.shapes; + const fakeShapeRefs = useRef({}); + + const bounds = calculateCanvasBounds(shapes); + const canvasSize = { + width: bounds.x + bounds.width, + height: bounds.y + bounds.height, + }; + const scaleFactorX = 200 / canvasSize.width; + const scaleFactorY = 180 / canvasSize.height; + const finalScale = Math.min(scaleFactorX, scaleFactorY); + + const { + showContextMenu, + contextMenuRef, + setShowContextMenu, + handleShowContextMenu, + } = useContextMenu(); + + return ( + <> +
onSetActivePage(page.id)} + onContextMenu={handleShowContextMenu} + > + + + {shapes.map(shape => { + if (!fakeShapeRefs.current[shape.id]) { + fakeShapeRefs.current[shape.id] = createRef(); + } + return renderShapeComponent(shape, { + handleSelected: () => {}, + shapeRefs: fakeShapeRefs, + handleDragEnd: + (_: string) => (_: KonvaEventObject) => {}, + handleTransform: () => {}, + }); + })} + + + {showContextMenu && ( + + )} +
+ + ); +}; diff --git a/src/pods/thumb-pages/index.ts b/src/pods/thumb-pages/index.ts new file mode 100644 index 00000000..864664f5 --- /dev/null +++ b/src/pods/thumb-pages/index.ts @@ -0,0 +1 @@ +export * from './thumb-pages.pod'; diff --git a/src/pods/thumb-pages/thumb-pages.module.css b/src/pods/thumb-pages/thumb-pages.module.css new file mode 100644 index 00000000..361dbc37 --- /dev/null +++ b/src/pods/thumb-pages/thumb-pages.module.css @@ -0,0 +1,17 @@ +.container { + display: flex; + padding: var(--space-md); + gap: var(--space-md); + align-items: center; + justify-content: center; + flex-wrap: wrap; + flex-direction: column; +} + +.activeThumb { + background-color: rgb(41, 41, 235); +} + +.activeText { + color: white; +} diff --git a/src/pods/thumb-pages/thumb-pages.pod.tsx b/src/pods/thumb-pages/thumb-pages.pod.tsx new file mode 100644 index 00000000..69261574 --- /dev/null +++ b/src/pods/thumb-pages/thumb-pages.pod.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import classes from './thumb-pages.module.css'; +import { useCanvasContext } from '@/core/providers'; +import { PageTitleInlineEdit, ThumbPage } from './components'; + +export const ThumbPagesPod: React.FC = () => { + const { fullDocument, addNewPage, setActivePage } = useCanvasContext(); + const [pageTitleBeingEdited, setPageTitleBeingEdited] = React.useState< + number | null + >(null); + const [activePageId, setActivePageId] = React.useState(null); + + const handleAddNewPage = () => { + addNewPage(); + }; + + const handleSetActivePage = (pageId: string) => { + setActivePage(pageId); + setActivePageId(pageId); + }; + + return ( +
+ {fullDocument.pages.map((page, index) => ( + +
+ + {pageTitleBeingEdited === index ? ( + + ) : ( +
setPageTitleBeingEdited(index)} + className={page.id === activePageId ? classes.activeText : ''} + > + {page.name} +
+ )} +
+
+ ))} + +
+ ); +}; diff --git a/src/pods/thumb-pages/use-context-menu-thumb.hook.tsx b/src/pods/thumb-pages/use-context-menu-thumb.hook.tsx new file mode 100644 index 00000000..5d5a65a6 --- /dev/null +++ b/src/pods/thumb-pages/use-context-menu-thumb.hook.tsx @@ -0,0 +1,42 @@ +import { useCanvasContext } from '@/core/providers'; +import { useEffect, useRef, useState } from 'react'; + +export const useContextMenu = () => { + const [showContextMenu, setShowContextMenu] = useState(false); + const contextMenuRef = useRef(null); + const { setIsThumbnailContextMenuVisible } = useCanvasContext(); + + const handleShowContextMenu = ( + event: React.MouseEvent + ) => { + event.preventDefault(); + if (!showContextMenu) { + setIsThumbnailContextMenuVisible(true); + setShowContextMenu(true); + } + }; + + useEffect(() => { + const closeContextMenu = (event: MouseEvent) => { + if ( + contextMenuRef.current && + !contextMenuRef.current.contains(event.target as Node) + ) { + setShowContextMenu(false); + setIsThumbnailContextMenuVisible(false); + } + }; + + window.addEventListener('mousedown', closeContextMenu); + return () => { + window.removeEventListener('mousedown', closeContextMenu); + }; + }, [showContextMenu, setIsThumbnailContextMenuVisible]); + + return { + showContextMenu, + contextMenuRef, + setShowContextMenu, + handleShowContextMenu, + }; +}; diff --git a/src/pods/toolbar/components/new-button/new-button.tsx b/src/pods/toolbar/components/new-button/new-button.tsx index fa694298..54501807 100644 --- a/src/pods/toolbar/components/new-button/new-button.tsx +++ b/src/pods/toolbar/components/new-button/new-button.tsx @@ -4,7 +4,7 @@ import { useCanvasContext } from '@/core/providers'; import { ToolbarButton } from '../toolbar-button'; export const NewButton = () => { - const { clearCanvas } = useCanvasContext(); + const { createNewFullDocument: clearCanvas } = useCanvasContext(); const handleClick = () => { clearCanvas(); diff --git a/src/scenes/main.scene.tsx b/src/scenes/main.scene.tsx index 086b26f9..93d74453 100644 --- a/src/scenes/main.scene.tsx +++ b/src/scenes/main.scene.tsx @@ -12,12 +12,17 @@ import { } from '@/pods'; import { PropertiesPod } from '@/pods/properties'; import { FooterPod } from '@/pods/footer/footer.pod'; +import { ThumbPagesPod } from '@/pods/thumb-pages'; export const MainScene = () => { return (
+
+ Pages + +
Devices