[Feature] Kanban board widget (#3049)
* init kanban board widget * kanban board * reverts to beautifully * kanban UI updates and dnd fixes * bugfix: when dropped outside the col, should return back to it inital position * updates min-width of the column * container and widget styles * style fixes: column container onDrag * adds button for new group * fixes new card overflow * add btn for adding cards * groups and cards updated * add property definition * improves draggable card position while drag is active * handle delete group/col * handle col/group title updates * handles editing card title * style fixes for input cursor * cleanup * card popover with codehinter fields * minor card fixes * updates exposed variable * simplify boardData into cols and cards * adds width and min-width style definations * build board from queries * handle draggable rbd-id * removes add group card and delete group option * fixes typos * show empty state message * fixes typos * removes card extra border color * fixes column typi and cards updates issue * adds enableAddCard property defination * adds accent color options * default style accent color * accent color fix * revets popover with hinter * fixes card drag and drop * removes hook * fixes: state synced with property defination updates(col and cards data) * fixes: on re-arranging the card via dnd, update the card content * handles if card columnId is updated * adds card container layer * clean up * dark theme * fixes card onDrop issue * renamed the exposed variable data --> lists * adds custom resolvers to the popover * handle widget crash when non iterables are passed * updates default card and col value * fixes dnd issues for dynamic card values * refactor: cleanup * handles empty and undefined cardData * fixes Height of widget is changing when popover thing is displayed. * fixes: updating card data in widget inspector * fixes: updating column data in widget inspector * fixes adding cards for newly created groups/columns * clean up * Add kanban event onCardAdded and expose lastAddedCard * Add onCardRemoved action and expsed lastRemovedCard variable * Add events and variables for card movement and selection * Add card edit feature for kanban widget * Rename lastAddedRemoved to lastRemovedCard in kanban * Rename lists to columns on kanban board * Set max height of kanban column to respond to widget height * Have "Add description" link if there is no description for Kanban cards * kanban docs * Change text from "add +" to "Add card" on kanban * Validate card data before update * Add tip about card id type on kanban documentation * Add default min width and width for kanban Co-authored-by: Sherfin Shamsudeen <sherfin94@gmail.com> Co-authored-by: Shubhendra <withshubh@gmail.com>
100
docs/docs/widgets/kanban.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
---
|
||||
id: kanban
|
||||
title: Kanban
|
||||
---
|
||||
|
||||
# Kanban
|
||||
|
||||
Kanban widget allows you to visually organize and prioritize your tasks with a transparent workflow. You can set the number of columns to display, enable/disable the add cards button, and bind data to the cards.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Events
|
||||
|
||||
To add an event, click on the widget handle to open the widget properties on the right sidebar. Go to the **Events** section and click on **Add handler**.
|
||||
|
||||
- [Card added](#card-added)
|
||||
- [Card removed](#card-removed)
|
||||
- [Card moved](#card-moved)
|
||||
- [Card selected](#card-selected)
|
||||
- [Card updated](#card-updated)
|
||||
|
||||
Just like any other event on ToolJet, you can set multiple handlers for any of the above mentioned events.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Properties
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
:::caution
|
||||
Please keep in mind that you need to provide an `id` for each card in the `Card data` field <br />
|
||||
and this `id` must be of type string.
|
||||
:::
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
| Properties | description | Expected value |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Columns | Enter the columns data - `id` and `title` in the form of array of objects or from a query that returns an array of objects. | `{{[{ "id": "1", "title": "to do" },{ "id": "2", "title": "in progress" },{ "id": "2", "title": "Completed" }]}}` or `{{queries.xyz.data}}` |
|
||||
| Card data | Enter the cards data - `id`, `title` and `columnId` in the form of array of objects or from a query that returns an array of objects. | `{{[{ id: "01", title: "one", columnId: "1" },{ id: "02", title: "two", columnId: "1" },{ id: "03", title: "three", columnId: "2" }]}}` or `{{queries.abc.data}}` |
|
||||
| Enable Add Card | This property allows you to show or hide the `Add Cards` button at the bottom of every column. | By deafult its enabled, you can programmatically set `{{true}}` or `{{false}}` enable/disable button by clicking on the `Fx` next to it |
|
||||
|
||||
## General
|
||||
|
||||
<b>Tooltip:</b> Set a tooltip text to specify the information about the data/kanban when the user moves the mouse pointer over the widget.
|
||||
|
||||
## Layout
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
| Layout | description | Expected value |
|
||||
| --------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| Show on desktop | Toggle on or off to display the widget in desktop view. | You can programmatically set the value by clicking on `Fx` to set the value `{{true}}` or `{{false}}` |
|
||||
| Show on mobile | Toggle on or off to display the widget in mobile view. | You can programmatically set the value by clicking on `Fx` to set the value `{{true}}` or `{{false}}` |
|
||||
|
||||
## Styles
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
| Style | Description |
|
||||
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Disable | If disabled or set to `{{false}}` the widget will be locked and becomes non-functional. By default, its disabled i.e. its value is set to `{{true}}` . |
|
||||
| Visibility | This is to control the visibility of the widget. If `{{false}}`/disabled the widget will not visible after the app is deployed. By default, it's enabled (set to `{{true}}`). |
|
||||
| Width | This property sets the width of the column. |
|
||||
| Accent color | You can change the accent color of the column title by entering the Hex color code or choosing a color of your choice from the color picker. |
|
||||
|
||||
## Exposed variables
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
| Variable | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| columns | The `columns` variable is an array of objects that includes the columns data in the respective objects. Since the columns variable is an array you'll need to specify the index of the object in the array to get the data within that object. Each object within a column has two keys - `id` and `title` and an array `cards` which is again an array of objects. Example: If you want to get the title of second card then you'll use `{{components.kanbanboard1.columns[1].title}}` - here we have specified the array index as `[1]` and then key which is the `title`. Similary you can get the card details using `{{components.kanbanboard1.columns[0].cards[1].title}}` |
|
||||
| lastAddedCard | The variable `lastAddedCard` holds the properties of the card that has been added lastly. It holds the following data - `id`, `title`, and `columnId` of the last addded card. You can get the values using `{{components.kanbanboard1.lastAddedCard.title}}` |
|
||||
| lastRemovedCard | The variable `lastRemovedCard` holds the properties of the card that has been recently deleted from the kanban. It holds the following data - `id`, `title`, and `columnId` of the recently deleted card. You can get the values using `{{components.kanbanboard1.lastRemovedCard.title}}` |
|
||||
| lastCardMovement | The variable `lastCardMovement` holds the properties of the card that has been recently moved from its original position. It holds the following data - `originColumnId`, `destinationColumnId`, `originCardIndex`, `destinationCardIndex` and an object `cardDetails` which includes `title`. You can get the values using `{{components.kanbanboard1.lastCardMovement.cardDetails.title}}` or `{{components.kanbanboard1.lastCardMovement.destinationCardIndex}}` |
|
||||
| lastUpdatedCard | The variable `lastUpdatedCard` holds `id`, `title`, and `columnId` of the latest modified card. You can get the values using `{{components.kanbanboard1.lastUpdatedCard.columnId}}` |
|
||||
| selectedCard | The variable `selectedCard` holds `id`, `title`, `columnId`, and `description` of the selected card in the kanban. You can get the values using `{{components.kanbanboard1.selectedCard.description}}` |
|
||||
|
|
@ -115,6 +115,7 @@ const sidebars = {
|
|||
'widgets/file-picker',
|
||||
'widgets/iframe',
|
||||
'widgets/image',
|
||||
'widgets/kanban',
|
||||
'widgets/listview',
|
||||
'widgets/map',
|
||||
'widgets/modal',
|
||||
|
|
|
|||
BIN
docs/static/img/widgets/kanban/kanban-events.png
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/static/img/widgets/kanban/kanban.png
vendored
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
docs/static/img/widgets/kanban/layout.png
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/static/img/widgets/kanban/properties.png
vendored
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/static/img/widgets/kanban/styles.png
vendored
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/static/img/widgets/kanban/variables.png
vendored
Normal file
|
After Width: | Height: | Size: 87 KiB |
5
frontend/assets/images/icons/editor/edit.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-pencil" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#597e8d" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 20h4l10.5 -10.5a1.5 1.5 0 0 0 -4 -4l-10.5 10.5v4"/>
|
||||
<line x1="13.5" y1="6.5" x2="17.5" y2="10.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 405 B |
8
frontend/assets/images/icons/widgets/kanbanboard.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-board-split" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#597e8d" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
<path d="M4 12h8" />
|
||||
<path d="M12 15h8" />
|
||||
<path d="M12 9h8" />
|
||||
<path d="M12 4v16" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 442 B |
181
frontend/package-lock.json
generated
|
|
@ -52,6 +52,7 @@
|
|||
"query-string": "^6.13.6",
|
||||
"rc-slider": "^9.7.5",
|
||||
"react": "^16.14.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-big-calendar": "^0.38.0",
|
||||
"react-bootstrap": "^1.5.2",
|
||||
"react-circular-progressbar": "^2.0.4",
|
||||
|
|
@ -17237,6 +17238,15 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/html-minifier-terser": {
|
||||
"version": "5.1.2",
|
||||
"integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w=="
|
||||
|
|
@ -17337,6 +17347,17 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-redux": {
|
||||
"version": "7.1.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz",
|
||||
"integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==",
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "^3.3.0",
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-transition-group": {
|
||||
"version": "4.4.1",
|
||||
"integrity": "sha512-vIo69qKKcYoJ8wKCJjwSgCTM+z3chw3g18dkrDfVX665tMH7tmbDxEAnPdey4gTlwZz5QuHGzd+hul0OVZDqqQ==",
|
||||
|
|
@ -19386,6 +19407,14 @@
|
|||
"urix": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-box-model": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||
"dependencies": {
|
||||
"tiny-invariant": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/css-loader": {
|
||||
"version": "6.5.1",
|
||||
"integrity": "sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==",
|
||||
|
|
@ -28359,6 +28388,11 @@
|
|||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/raf-schd": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
|
|
@ -28519,6 +28553,24 @@
|
|||
"pure-color": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-beautiful-dnd": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
|
||||
"integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"css-box-model": "^1.2.0",
|
||||
"memoize-one": "^5.1.1",
|
||||
"raf-schd": "^4.0.2",
|
||||
"react-redux": "^7.2.0",
|
||||
"redux": "^4.0.4",
|
||||
"use-memo-one": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.5 || ^17.0.0",
|
||||
"react-dom": "^16.8.5 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-big-calendar": {
|
||||
"version": "0.38.0",
|
||||
"integrity": "sha512-eoVkt9gTo+f1HBL09+o7dYLxp6QxHv52fcn50P5PfaWp3S98uGLQqoqsvghT85koMKvGfDVa5V0+J7yHcaF07Q==",
|
||||
|
|
@ -29079,6 +29131,46 @@
|
|||
"react-dom": "~16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "7.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.8.tgz",
|
||||
"integrity": "sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@types/react-redux": "^7.1.20",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^17.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.3 || ^17 || ^18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux/node_modules/@babel/runtime": {
|
||||
"version": "7.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz",
|
||||
"integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux/node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"node_modules/react-rnd": {
|
||||
"version": "10.3.0",
|
||||
"integrity": "sha512-v+0TRPIaRWY25TYv02vLQHYpACbkX+4xKvsyIrUEy4bMpq0bP1oEiaxTorp0Xn72IVv0QZV1vOnZimgTEB/skw==",
|
||||
|
|
@ -31033,6 +31125,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-memo-one": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
|
||||
"integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.10.3",
|
||||
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
|
||||
|
|
@ -45089,6 +45189,15 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/hoist-non-react-statics": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||
"requires": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"@types/html-minifier-terser": {
|
||||
"version": "5.1.2",
|
||||
"integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w=="
|
||||
|
|
@ -45189,6 +45298,17 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-redux": {
|
||||
"version": "7.1.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz",
|
||||
"integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==",
|
||||
"requires": {
|
||||
"@types/hoist-non-react-statics": "^3.3.0",
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "4.4.1",
|
||||
"integrity": "sha512-vIo69qKKcYoJ8wKCJjwSgCTM+z3chw3g18dkrDfVX665tMH7tmbDxEAnPdey4gTlwZz5QuHGzd+hul0OVZDqqQ==",
|
||||
|
|
@ -46747,6 +46867,14 @@
|
|||
"urix": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"css-box-model": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||
"requires": {
|
||||
"tiny-invariant": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"css-loader": {
|
||||
"version": "6.5.1",
|
||||
"integrity": "sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==",
|
||||
|
|
@ -53354,6 +53482,11 @@
|
|||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"raf-schd": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||
},
|
||||
"randombytes": {
|
||||
"version": "2.1.0",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
|
|
@ -53470,6 +53603,20 @@
|
|||
"pure-color": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"react-beautiful-dnd": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
|
||||
"integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"css-box-model": "^1.2.0",
|
||||
"memoize-one": "^5.1.1",
|
||||
"raf-schd": "^4.0.2",
|
||||
"react-redux": "^7.2.0",
|
||||
"redux": "^4.0.4",
|
||||
"use-memo-one": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"react-big-calendar": {
|
||||
"version": "0.38.0",
|
||||
"integrity": "sha512-eoVkt9gTo+f1HBL09+o7dYLxp6QxHv52fcn50P5PfaWp3S98uGLQqoqsvghT85koMKvGfDVa5V0+J7yHcaF07Q==",
|
||||
|
|
@ -53884,6 +54031,34 @@
|
|||
"webrtc-adapter": "^7.2.1"
|
||||
}
|
||||
},
|
||||
"react-redux": {
|
||||
"version": "7.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.8.tgz",
|
||||
"integrity": "sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@types/react-redux": "^7.1.20",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^17.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz",
|
||||
"integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-rnd": {
|
||||
"version": "10.3.0",
|
||||
"integrity": "sha512-v+0TRPIaRWY25TYv02vLQHYpACbkX+4xKvsyIrUEy4bMpq0bP1oEiaxTorp0Xn72IVv0QZV1vOnZimgTEB/skw==",
|
||||
|
|
@ -55350,6 +55525,12 @@
|
|||
"use-isomorphic-layout-effect": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"use-memo-one": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
|
||||
"integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"util": {
|
||||
"version": "0.10.3",
|
||||
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
"query-string": "^6.13.6",
|
||||
"rc-slider": "^9.7.5",
|
||||
"react": "^16.14.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-big-calendar": "^0.38.0",
|
||||
"react-bootstrap": "^1.5.2",
|
||||
"react-circular-progressbar": "^2.0.4",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { ButtonGroup } from './Components/ButtonGroup';
|
|||
import { CustomComponent } from './Components/CustomComponent/CustomComponent';
|
||||
import { VerticalDivider } from './Components/verticalDivider';
|
||||
import { PDF } from './Components/PDF';
|
||||
import { KanbanBoard } from './Components/KanbanBoard/KanbanBoard';
|
||||
import { Steps } from './Components/Steps';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import '@/_styles/custom.scss';
|
||||
|
|
@ -91,6 +92,7 @@ const AllComponents = {
|
|||
CustomComponent,
|
||||
VerticalDivider,
|
||||
PDF,
|
||||
KanbanBoard,
|
||||
Steps,
|
||||
};
|
||||
|
||||
|
|
|
|||
126
frontend/src/Editor/Components/KanbanBoard/Board.jsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import React from 'react';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import Column from './Column';
|
||||
import { reorderCards, moveCards } from './utils';
|
||||
|
||||
const grid = 8;
|
||||
|
||||
const getItemStyle = (isDragging, draggableStyle) => {
|
||||
const _draggableStyle = isDragging
|
||||
? { ...draggableStyle, left: draggableStyle.left - 100, top: draggableStyle.top - 100 }
|
||||
: draggableStyle;
|
||||
|
||||
return {
|
||||
..._draggableStyle,
|
||||
userSelect: 'none',
|
||||
padding: grid * 2,
|
||||
margin: `0 0 ${grid}px 0`,
|
||||
background: isDragging ? '#c2cfff' : '#fefefe',
|
||||
};
|
||||
};
|
||||
|
||||
function Board({ height, state, colStyles, setState, fireEvent, setExposedVariable }) {
|
||||
const addNewItem = (state, keyIndex) => {
|
||||
const newItem = {
|
||||
id: uuidv4(),
|
||||
title: 'New card',
|
||||
columnId: state[keyIndex].id,
|
||||
};
|
||||
const newState = [...state];
|
||||
if (!newState[keyIndex]['cards']) [(newState[keyIndex]['cards'] = [])];
|
||||
newState[keyIndex]['cards'].push(newItem);
|
||||
setState(newState);
|
||||
setExposedVariable('lastAddedCard', newItem).then(() => fireEvent('onCardAdded'));
|
||||
};
|
||||
|
||||
function onDragEnd(result) {
|
||||
const { source, destination } = result;
|
||||
|
||||
// dropped outside the list
|
||||
if (destination && destination !== null) {
|
||||
const sInd = +source.droppableId;
|
||||
const dInd = +destination.droppableId;
|
||||
const originColumnId = state[sInd].id;
|
||||
const destinationColumnId = state[dInd].id;
|
||||
|
||||
const card = state[sInd]['cards'][source.index];
|
||||
const cardDetails = {
|
||||
title: card.title,
|
||||
};
|
||||
|
||||
if (sInd === dInd) {
|
||||
const items = reorderCards(state[sInd]['cards'], source.index, destination.index);
|
||||
const newState = [...state];
|
||||
newState[sInd]['cards'] = items;
|
||||
setState(newState);
|
||||
} else {
|
||||
const result = moveCards(state[sInd]['cards'], state[dInd].cards, source, destination);
|
||||
const newState = [...state];
|
||||
newState[sInd]['cards'] = result[sInd];
|
||||
newState[dInd]['cards'] = result[dInd];
|
||||
newState[dInd]['cards'][destination.index].columnId = newState[dInd].id;
|
||||
|
||||
setState(newState);
|
||||
}
|
||||
|
||||
const movementDetails = {
|
||||
originColumnId,
|
||||
destinationColumnId,
|
||||
originCardIndex: sInd,
|
||||
destinationCardIndex: dInd,
|
||||
cardDetails,
|
||||
};
|
||||
setExposedVariable('lastCardMovement', movementDetails).then(() => fireEvent('onCardMoved'));
|
||||
}
|
||||
}
|
||||
|
||||
const getListStyle = (isDraggingOver) => ({
|
||||
...colStyles,
|
||||
padding: grid,
|
||||
borderColor: isDraggingOver && '#c0ccf8',
|
||||
});
|
||||
|
||||
const updateCardProperty = (columnIndex, cardIndex, property, newValue) => {
|
||||
const columnOfCardToBeUpdated = state[columnIndex];
|
||||
const cardSetOfTheCardToBeUpdated = columnOfCardToBeUpdated.cards;
|
||||
const cardToBeUpdated = cardSetOfTheCardToBeUpdated[cardIndex];
|
||||
const updatedCard = { ...cardToBeUpdated, [property]: newValue };
|
||||
const updatedCardSet = cardSetOfTheCardToBeUpdated.map((card, index) => (index === cardIndex ? updatedCard : card));
|
||||
const updatedColumn = { ...columnOfCardToBeUpdated, cards: updatedCardSet };
|
||||
const newState = state.map((column, index) => (index === columnIndex ? updatedColumn : column));
|
||||
setState(newState);
|
||||
|
||||
setExposedVariable('lastUpdatedCard', updatedCard).then(() => fireEvent('onCardUpdated'));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: height, overflowX: 'auto' }}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className="container d-flex"
|
||||
>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{state.map((col, ind) => (
|
||||
<Column
|
||||
key={ind}
|
||||
state={state}
|
||||
group={col}
|
||||
keyIndex={ind}
|
||||
getListStyle={getListStyle}
|
||||
getItemStyle={getItemStyle}
|
||||
updateCb={setState}
|
||||
addNewItem={addNewItem}
|
||||
colStyles={colStyles}
|
||||
fireEvent={fireEvent}
|
||||
setExposedVariable={setExposedVariable}
|
||||
updateCardProperty={updateCardProperty}
|
||||
boardHeight={height}
|
||||
/>
|
||||
))}
|
||||
</DragDropContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Board;
|
||||
111
frontend/src/Editor/Components/KanbanBoard/Card.jsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import React from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { BoardContext } from './KanbanBoard';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { CardEventPopover } from './CardPopover';
|
||||
import { ReactPortal } from '@/_components/Portal/ReactPortal';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const Card = ({
|
||||
item,
|
||||
index,
|
||||
state,
|
||||
updateCb,
|
||||
getItemStyle,
|
||||
keyIndex,
|
||||
fireEvent,
|
||||
setExposedVariable,
|
||||
updateCardProperty,
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
|
||||
const [eventPopoverOptions, setEventPopoverOptions] = React.useState({ show: false });
|
||||
|
||||
function popoverClosed() {
|
||||
setEventPopoverOptions({
|
||||
...eventPopoverOptions,
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
const { id, darkMode } = React.useContext(BoardContext);
|
||||
|
||||
const removeCardHandler = (colIndex, cardIndex) => {
|
||||
const newState = [...state];
|
||||
const removedCard = newState[colIndex]['cards'].splice(cardIndex, 1)[0];
|
||||
updateCb(newState);
|
||||
setExposedVariable('lastRemovedCard', removedCard).then(() => fireEvent('onCardRemoved'));
|
||||
};
|
||||
|
||||
const draggableId = item.id ?? uuidv4();
|
||||
|
||||
const handleEventPopoverOptions = (e) => {
|
||||
setEventPopoverOptions({
|
||||
...eventPopoverOptions,
|
||||
show: true,
|
||||
offset: {
|
||||
left: e.target.getBoundingClientRect().x,
|
||||
top: e.target.getBoundingClientRect().y,
|
||||
width: e.target.getBoundingClientRect().width,
|
||||
height: e.target.getBoundingClientRect().height,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCardClick = (event) => {
|
||||
handleEventPopoverOptions(event);
|
||||
setExposedVariable('selectedCard', item).then(() => fireEvent('onCardSelected'));
|
||||
};
|
||||
|
||||
const target = React.useRef(null);
|
||||
const el = document.getElementById(id);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={item.id}
|
||||
draggableId={typeof draggableId !== String ? String(draggableId) : draggableId}
|
||||
index={index}
|
||||
>
|
||||
{(dndProps, dndState) => (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={`dnd-card card card-sm ${darkMode && 'card-dark'}`}
|
||||
ref={dndProps.innerRef}
|
||||
{...dndProps.draggableProps}
|
||||
{...dndProps.dragHandleProps}
|
||||
style={{ ...getItemStyle(dndState.isDragging, dndProps.draggableProps.style) }}
|
||||
>
|
||||
<div className="card-body d-flex">
|
||||
<span ref={target} onClick={handleCardClick} className="text-muted flex-grow-1 cursor-pointer fw-bold">
|
||||
{item.title}
|
||||
</span>
|
||||
{isHovered && !item.isEditing && (
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
type="btn btn-sm btn-danger"
|
||||
onClick={() => removeCardHandler(keyIndex, index)}
|
||||
>
|
||||
<img className="mx-1" src={`/assets/images/icons/trash.svg`} width={12} height={12} />
|
||||
</span>
|
||||
)}
|
||||
{eventPopoverOptions.show && (
|
||||
<ReactPortal parent={el} className="kanban-portal" componentName="kanban">
|
||||
<CardEventPopover
|
||||
kanbanCardWidgetId={id}
|
||||
show={eventPopoverOptions.show}
|
||||
offset={eventPopoverOptions.offset}
|
||||
popoverClosed={popoverClosed}
|
||||
card={item}
|
||||
updateCardProperty={updateCardProperty}
|
||||
index={index}
|
||||
keyIndex={keyIndex}
|
||||
/>
|
||||
</ReactPortal>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
157
frontend/src/Editor/Components/KanbanBoard/CardPopover.jsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export const CardEventPopover = function ({
|
||||
show,
|
||||
offset,
|
||||
kanbanCardWidgetId,
|
||||
popoverClosed,
|
||||
card,
|
||||
updateCardProperty,
|
||||
index,
|
||||
keyIndex,
|
||||
}) {
|
||||
const parentRef = useRef(null);
|
||||
const [showPopover, setShow] = useState(show);
|
||||
const [top, setTop] = useState(0);
|
||||
const [left, setLeft] = useState(0);
|
||||
|
||||
const [titleInputBoxValue, setTitleInputBoxValue] = useState(card.title ?? '');
|
||||
const [descriptionTextAreaValue, setDescriptionTextAreaValue] = useState(card.description ?? '');
|
||||
const [titleHovered, setTitleHovered] = useState(false);
|
||||
const [descriptionHovered, setDescriptionHovered] = useState(false);
|
||||
const [titleEditMode, setTitleEditMode] = useState(false);
|
||||
const [descriptionEditMode, setDescriptionEditMode] = useState(false);
|
||||
|
||||
const minHeight = 400;
|
||||
let kanbanBounds;
|
||||
|
||||
const kanbanElement = document.getElementById(kanbanCardWidgetId);
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (parentRef.current && !parentRef.current.contains(event.target)) {
|
||||
popoverClosed();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleClickOutside, true);
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setShow(show);
|
||||
}, [show]);
|
||||
|
||||
useEffect(() => {
|
||||
if (offset?.top && showPopover) {
|
||||
const _left = offset.left - kanbanBounds.x + offset.width;
|
||||
const _top = ((offset.top - kanbanBounds.y) * 100) / kanbanBounds.height;
|
||||
setTop(_top);
|
||||
setLeft(_left);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [offset?.top, showPopover]);
|
||||
|
||||
if (kanbanElement && showPopover) {
|
||||
kanbanBounds = kanbanElement.getBoundingClientRect();
|
||||
}
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 100,
|
||||
width: '300px',
|
||||
maxWidth: '300px',
|
||||
height: 300,
|
||||
top: `${top}%`,
|
||||
left,
|
||||
display: showPopover ? 'block' : 'none',
|
||||
}}
|
||||
role="tooltip"
|
||||
x-placement="left"
|
||||
className={`popover bs-popover-left shadow-lg ${darkMode && 'popover-dark-themed theme-dark'}`}
|
||||
ref={parentRef}
|
||||
id={`${kanbanCardWidgetId}-popover`}
|
||||
>
|
||||
{parentRef.current && showPopover && (
|
||||
<div className="popover-body" style={{ padding: 'unset', width: '100%', height: 100, zIndex: 11 }}>
|
||||
<div className="rows p-2 overflow-auto">
|
||||
<div
|
||||
className="row overflow-auto"
|
||||
onMouseEnter={() => setTitleHovered(true)}
|
||||
onMouseLeave={() => setTitleHovered(false)}
|
||||
>
|
||||
{titleEditMode ? (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
defaultValue={titleInputBoxValue}
|
||||
onChange={(event) => setTitleInputBoxValue(event.target.value)}
|
||||
onBlur={() => {
|
||||
updateCardProperty(keyIndex, index, 'title', titleInputBoxValue);
|
||||
setTitleEditMode(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<h3>
|
||||
{card?.title ?? ''}
|
||||
<img
|
||||
src="/assets/images/icons/editor/edit.svg"
|
||||
style={{ visibility: titleHovered ? 'visible' : 'hidden', height: 15, width: 15, paddingLeft: 1 }}
|
||||
onClick={() => setTitleEditMode(true)}
|
||||
/>
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="row overflow-auto d-flex align-items-center flex-column"
|
||||
onMouseEnter={() => setDescriptionHovered(true)}
|
||||
onMouseLeave={() => setDescriptionHovered(false)}
|
||||
style={{ maxHeight: 250 }}
|
||||
>
|
||||
{descriptionEditMode ? (
|
||||
<textarea
|
||||
className="form-control"
|
||||
style={{ width: '95%' }}
|
||||
onChange={(event) => setDescriptionTextAreaValue(event.target.value)}
|
||||
onBlur={() => {
|
||||
updateCardProperty(keyIndex, index, 'description', descriptionTextAreaValue);
|
||||
setDescriptionEditMode(false);
|
||||
}}
|
||||
rows={10}
|
||||
>
|
||||
{descriptionTextAreaValue}
|
||||
</textarea>
|
||||
) : (
|
||||
<p>
|
||||
{['', undefined].includes(card.description) ? (
|
||||
<a style={{ color: 'grey' }} onClick={() => setDescriptionEditMode(true)}>
|
||||
Add description
|
||||
</a>
|
||||
) : (
|
||||
card.description
|
||||
)}
|
||||
<img
|
||||
src="/assets/images/icons/editor/edit.svg"
|
||||
style={{
|
||||
visibility: descriptionHovered ? 'visible' : 'hidden',
|
||||
height: 15,
|
||||
width: 15,
|
||||
paddingLeft: 1,
|
||||
}}
|
||||
onClick={() => setDescriptionEditMode(true)}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
128
frontend/src/Editor/Components/KanbanBoard/Column.jsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import React from 'react';
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import { Card } from './Card';
|
||||
import { BoardContext } from './KanbanBoard';
|
||||
|
||||
const Column = ({
|
||||
state,
|
||||
group,
|
||||
keyIndex,
|
||||
getListStyle,
|
||||
getItemStyle,
|
||||
updateCb,
|
||||
addNewItem,
|
||||
fireEvent,
|
||||
setExposedVariable,
|
||||
updateCardProperty,
|
||||
boardHeight,
|
||||
}) => {
|
||||
const styles = {
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'hidden',
|
||||
maxHeight: boardHeight - 80,
|
||||
};
|
||||
|
||||
const cards = group['cards'];
|
||||
|
||||
const updateGroupTitle = (newTitle) => {
|
||||
const newState = [...state];
|
||||
newState[keyIndex]['title'] = newTitle;
|
||||
updateCb(newState);
|
||||
};
|
||||
|
||||
const flipTitleToEditMode = (index) => {
|
||||
const newState = [...state];
|
||||
const isEditing = newState[index]['isEditing'];
|
||||
|
||||
if (isEditing === true) {
|
||||
newState[index]['isEditing'] = false;
|
||||
} else {
|
||||
newState[index]['isEditing'] = true;
|
||||
}
|
||||
updateCb(newState);
|
||||
};
|
||||
|
||||
const { enableAddCard, accentColor, darkMode } = React.useContext(BoardContext);
|
||||
|
||||
const hexaCodeToRgb = (hex) => {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
|
||||
return `rgba(${r},${g},${b},0.2)`;
|
||||
};
|
||||
|
||||
const colAccentColor = {
|
||||
color: accentColor ?? '#4d72fa',
|
||||
backgroundColor: accentColor ? hexaCodeToRgb(accentColor) : hexaCodeToRgb('#4d72fa'),
|
||||
};
|
||||
|
||||
return (
|
||||
<Droppable key={keyIndex} droppableId={String(keyIndex)}>
|
||||
{(dndProps, dndState) => (
|
||||
<div
|
||||
className={`card text-dark mb-3 m-2 kanban-column ${darkMode ? 'bg-dark' : 'bg-light'}`}
|
||||
ref={dndProps.innerRef}
|
||||
style={getListStyle(dndState.isDraggingOver)}
|
||||
{...dndProps.droppableProps}
|
||||
>
|
||||
<div className="card-header d-flex">
|
||||
<div className="flex-grow-1 ">
|
||||
{group['isEditing'] ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
defaultValue={group['title']}
|
||||
autoFocus={true}
|
||||
onBlur={(e) => {
|
||||
updateGroupTitle(e.target.value);
|
||||
flipTitleToEditMode(keyIndex);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
updateGroupTitle(e.target.value);
|
||||
flipTitleToEditMode(keyIndex);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
style={colAccentColor}
|
||||
onClick={() => flipTitleToEditMode(keyIndex)}
|
||||
className="bade-component cursor-text"
|
||||
>
|
||||
{group.title}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ ...styles }} className="card-body">
|
||||
{cards?.map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
item={item}
|
||||
index={index}
|
||||
state={state}
|
||||
updateCb={updateCb}
|
||||
getItemStyle={getItemStyle}
|
||||
keyIndex={keyIndex}
|
||||
fireEvent={fireEvent}
|
||||
setExposedVariable={setExposedVariable}
|
||||
updateCardProperty={updateCardProperty}
|
||||
/>
|
||||
))}
|
||||
|
||||
{dndProps.placeholder}
|
||||
{enableAddCard && (
|
||||
<button className="btn btn-primary w-100 add-card-btn" onClick={() => addNewItem(state, keyIndex)}>
|
||||
Add card
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
|
||||
export default Column;
|
||||
131
frontend/src/Editor/Components/KanbanBoard/KanbanBoard.jsx
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import Board from './Board';
|
||||
import { isCardColoumnIdUpdated, updateCardData, updateColumnData, getData, isArray, isValidCardData } from './utils';
|
||||
|
||||
export const BoardContext = React.createContext({});
|
||||
|
||||
export const KanbanBoard = ({
|
||||
id,
|
||||
height,
|
||||
properties,
|
||||
styles,
|
||||
currentState,
|
||||
setExposedVariable,
|
||||
containerProps,
|
||||
removeComponent,
|
||||
fireEvent,
|
||||
}) => {
|
||||
const { columns, cardData, enableAddCard } = properties;
|
||||
|
||||
const { visibility, disabledState, width, minWidth, accentColor } = styles;
|
||||
|
||||
const [rawColumnData, setRawColumnData] = React.useState([]);
|
||||
const [rawCardData, setRawCardData] = React.useState([]);
|
||||
|
||||
const [state, setState] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setExposedVariable('columns', state);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isArray(rawColumnData) || isArray(rawCardData)) {
|
||||
const colData = JSON.parse(JSON.stringify(columns));
|
||||
const _cardData = JSON.parse(JSON.stringify(cardData));
|
||||
setRawColumnData(colData);
|
||||
setRawCardData(_cardData);
|
||||
const data = getData(colData, _cardData);
|
||||
setState(data);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (JSON.stringify(columns) !== JSON.stringify(rawColumnData) && isArray(columns)) {
|
||||
const newData = updateColumnData(state, rawColumnData, columns);
|
||||
|
||||
if (newData && isArray(newData)) {
|
||||
setState(newData);
|
||||
}
|
||||
|
||||
if (!newData && columns.length !== rawColumnData.length) {
|
||||
setState(() => getData(columns, rawCardData));
|
||||
}
|
||||
setRawColumnData(columns);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [columns]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isValidCardData(cardData)) {
|
||||
if (cardData.length !== rawCardData.length) {
|
||||
setState(() => getData(columns, cardData));
|
||||
} else if (JSON.stringify(cardData) !== JSON.stringify(rawCardData) && isArray(cardData)) {
|
||||
if (cardData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isColumnIdUpdated = isCardColoumnIdUpdated(rawCardData, cardData);
|
||||
|
||||
if (isColumnIdUpdated) {
|
||||
const newData = getData(columns, cardData);
|
||||
if (newData && isArray(newData)) {
|
||||
setState(newData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isColumnIdUpdated) {
|
||||
const newData = updateCardData(state, rawCardData, cardData);
|
||||
|
||||
if (newData && isArray(newData)) {
|
||||
setState(newData);
|
||||
}
|
||||
if (newData === null) {
|
||||
return setState(() => getData(columns, cardData));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setRawCardData(cardData);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cardData]);
|
||||
|
||||
const colStyles = {
|
||||
width: !width ? '100%' : width,
|
||||
minWidth: !minWidth ? '350px' : minWidth,
|
||||
};
|
||||
|
||||
if (!state || state.length === 0) {
|
||||
return (
|
||||
<div className="mx-auto w-50 p-5 bg-light no-components-box" style={{ marginTop: '15%' }}>
|
||||
<center className="text-muted">Board is empty.</center>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
return (
|
||||
<BoardContext.Provider
|
||||
value={{ id, currentState, enableAddCard, accentColor, containerProps, removeComponent, darkMode }}
|
||||
>
|
||||
<div
|
||||
id={id}
|
||||
style={{ display: visibility ? '' : 'none' }}
|
||||
data-disabled={disabledState}
|
||||
className={`kanban-container p-0 ${darkMode ? 'dark-themed' : ''}`}
|
||||
>
|
||||
<Board
|
||||
height={height}
|
||||
state={state}
|
||||
isDisable={disabledState}
|
||||
colStyles={colStyles}
|
||||
setState={setState}
|
||||
fireEvent={fireEvent}
|
||||
setExposedVariable={setExposedVariable}
|
||||
/>
|
||||
</div>
|
||||
</BoardContext.Provider>
|
||||
);
|
||||
};
|
||||
140
frontend/src/Editor/Components/KanbanBoard/utils.js
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
export const getData = (columns, cards) => {
|
||||
if (isArray(cards) && isArray(columns)) {
|
||||
const clonedColumns = [...columns];
|
||||
cards.forEach((card) => {
|
||||
const column = clonedColumns.find((column) => column.id === card.columnId);
|
||||
if (column) {
|
||||
column['cards'] = column?.cards ? [...column.cards, card] : [card];
|
||||
}
|
||||
});
|
||||
|
||||
return clonedColumns;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const reorderCards = (list, startIndex, endIndex) => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const moveCards = (source, destination, droppableSource, droppableDestination) => {
|
||||
const sourceClone = Array.from(source);
|
||||
const destinationClone = destination ? Array.from(destination) : [];
|
||||
const [removed] = sourceClone.splice(droppableSource.index, 1);
|
||||
|
||||
destinationClone.splice(droppableDestination.index, 0, removed);
|
||||
|
||||
const result = {};
|
||||
result[droppableSource.droppableId] = sourceClone;
|
||||
result[droppableDestination.droppableId] = destinationClone;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const diffCol = (next, current) => {
|
||||
const nextState = [...next];
|
||||
const currentState = [...current];
|
||||
const diff = [];
|
||||
|
||||
nextState.forEach((col, index) => {
|
||||
const curr = col;
|
||||
const next = currentState[index];
|
||||
|
||||
const isDiff = curr.id === next?.id && curr.title === next.title;
|
||||
if (!isDiff && next) {
|
||||
const newCol = {
|
||||
...next,
|
||||
id: curr.id,
|
||||
title: curr.title,
|
||||
};
|
||||
diff.push(newCol);
|
||||
}
|
||||
});
|
||||
return diff;
|
||||
};
|
||||
|
||||
export const updateColumnData = (currentData, column, newData) => {
|
||||
const diff = diffCol(newData, currentData);
|
||||
|
||||
if (diff.length === 0) return null;
|
||||
|
||||
const nextState = [...currentData];
|
||||
diff.forEach((col) => {
|
||||
const index = nextState.findIndex((c) => c.id === col.id);
|
||||
nextState[index] = col;
|
||||
});
|
||||
return nextState;
|
||||
};
|
||||
|
||||
const cardDiffExits = (currentCards, newCards, state) => {
|
||||
const diff = [];
|
||||
|
||||
if (!currentCards) return null;
|
||||
|
||||
newCards.forEach((card) => {
|
||||
const index = currentCards.findIndex((c) => c.id === card.id);
|
||||
const updatedColumnId = findCard(state, card.id)?.columnId;
|
||||
|
||||
if (index !== -1) {
|
||||
const newCard = {
|
||||
...card,
|
||||
columnId: updatedColumnId,
|
||||
};
|
||||
diff.push(newCard);
|
||||
}
|
||||
});
|
||||
return diff;
|
||||
};
|
||||
|
||||
export const updateCardData = (currentData, cards, newData) => {
|
||||
const diffing = cardDiffExits(cards, newData, currentData);
|
||||
if (!diffing || diffing.length === 0) return null;
|
||||
|
||||
const newState = [...currentData];
|
||||
diffing.forEach((card) => {
|
||||
const colIndex = newState.findIndex((c) => c.id === card.columnId);
|
||||
const cardIndex = newState[colIndex].cards.findIndex((c) => c.id === card.id);
|
||||
newState[colIndex].cards[cardIndex] = card;
|
||||
});
|
||||
return newState;
|
||||
};
|
||||
|
||||
const findCard = (state, cardId) => {
|
||||
for (let i = 0; i < state.length; i++) {
|
||||
for (let j = 0; j < state[i].cards?.length ?? 0; j++) {
|
||||
if (state[i].cards[j].id === cardId) {
|
||||
return state[i].cards[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const isCardColoumnIdUpdated = (currentCardData, nextCardData) => {
|
||||
const currentState = [...currentCardData];
|
||||
const nextState = [...nextCardData];
|
||||
|
||||
let isColoumnIdUpdated = false;
|
||||
|
||||
currentState.forEach((card, index) => {
|
||||
if (nextState[index]) {
|
||||
const prevColId = card.columnId;
|
||||
const newColId = nextState[index].columnId;
|
||||
if (prevColId !== newColId) {
|
||||
isColoumnIdUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return isColoumnIdUpdated;
|
||||
};
|
||||
|
||||
export const isArray = (value) => Object.prototype.toString.call(value).slice(8, -1) === 'Array';
|
||||
|
||||
export const isValidCardData = (cardData) => {
|
||||
return _.isArray(cardData) && cardData.every((card) => _.isString(card.id));
|
||||
};
|
||||
|
|
@ -236,7 +236,7 @@ export const DraggableBox = function DraggableBox({
|
|||
setDragging(false);
|
||||
onDragStop(e, id, direction, currentLayout, currentLayoutOptions);
|
||||
}}
|
||||
cancel={`div.table-responsive.jet-data-table, div.calendar-widget, div.text-input, .textarea, .map-widget, .range-slider`}
|
||||
cancel={`div.table-responsive.jet-data-table, div.calendar-widget, div.text-input, .textarea, .map-widget, .range-slider, .kanban-container`}
|
||||
onDragStart={(e) => e.stopPropagation()}
|
||||
onResizeStop={(e, direction, ref, d, position) => {
|
||||
setResizing(false);
|
||||
|
|
|
|||
|
|
@ -2384,4 +2384,70 @@ ReactDOM.render(<ConnectedComponent />, document.body);`,
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'KanbanBoard',
|
||||
displayName: 'Kanban Board',
|
||||
description: 'Kanban Board',
|
||||
component: 'KanbanBoard',
|
||||
defaultSize: {
|
||||
width: 40,
|
||||
height: 490,
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
properties: {
|
||||
columns: { type: 'code', displayName: 'Columns' },
|
||||
cardData: { type: 'code', displayName: 'Card Data' },
|
||||
enableAddCard: { type: 'toggle', displayName: 'Enable Add Card' },
|
||||
},
|
||||
events: {
|
||||
onCardAdded: { displayName: 'Card added' },
|
||||
onCardRemoved: { displayName: 'Card removed' },
|
||||
onCardMoved: { displayName: 'Card moved' },
|
||||
onCardSelected: { displayName: 'Card selected' },
|
||||
onCardUpdated: { displayName: 'Card updated' },
|
||||
},
|
||||
styles: {
|
||||
disabledState: { type: 'toggle', displayName: 'Disable' },
|
||||
visibility: { type: 'toggle', displayName: 'Visibility' },
|
||||
width: { type: 'number', displayName: 'Width' },
|
||||
minWidth: { type: 'number', displayName: 'Min Width' },
|
||||
accentColor: { type: 'color', displayName: 'Accent color' },
|
||||
},
|
||||
exposedVariables: {
|
||||
columns: {},
|
||||
lastAddedCard: {},
|
||||
lastRemovedCard: {},
|
||||
lastCardMovement: {},
|
||||
lastUpdatedCard: {},
|
||||
},
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
showOnMobile: { value: '{{false}}' },
|
||||
},
|
||||
properties: {
|
||||
columns: {
|
||||
value: '{{[{ "id": "1", "title": "to do" },{ "id": "2", "title": "in progress" }]}}',
|
||||
},
|
||||
cardData: {
|
||||
value:
|
||||
'{{[{ id: "01", title: "one", columnId: "1" },{ id: "02", title: "two", columnId: "1" },{ id: "03", title: "three", columnId: "2" }]}}',
|
||||
},
|
||||
enableAddCard: {
|
||||
value: `{{true}}`,
|
||||
},
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
width: { value: '{{400}}' },
|
||||
minWidth: { value: '{{200}}' },
|
||||
textColor: { value: '#4d72fa' },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -493,6 +493,11 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
|
|||
'onCalendarViewChange',
|
||||
'onSearchTextChanged',
|
||||
'onPageChange',
|
||||
'onCardAdded',
|
||||
'onCardRemoved',
|
||||
'onCardMoved',
|
||||
'onCardSelected',
|
||||
'onCardUpdated',
|
||||
'onTabSwitch',
|
||||
].includes(eventName)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -5320,6 +5320,120 @@ div#driver-page-overlay {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Kanban board
|
||||
|
||||
.kanban-container.dark-themed {
|
||||
background-color: $bg-dark-light !important;
|
||||
.kanban-column {
|
||||
.card-header {
|
||||
background-color: #324156 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-container {
|
||||
background-color: #fefefe;
|
||||
|
||||
.kanban-column {
|
||||
background-color: #f4f4f4;
|
||||
padding: 0 !important;
|
||||
height: fit-content !important;
|
||||
|
||||
.card-body {
|
||||
&:hover {
|
||||
overflow-y: auto !important;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #fefefe;
|
||||
|
||||
.badge {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body .dnd-card {
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
.dnd-card.card {
|
||||
height: 52px !important;
|
||||
padding: 5px !important;
|
||||
}
|
||||
.dnd-card.card.card-dark {
|
||||
background-color: $bg-dark !important;
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-board-add-group {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
background-color: transparent;
|
||||
border-style: dashed;
|
||||
border-color: rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-auto-rows: max-content;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
appearance: none;
|
||||
outline: none;
|
||||
margin: 10px;
|
||||
border-radius: 5px;
|
||||
min-width: 350px;
|
||||
height: 200px;
|
||||
font-size: 1em;
|
||||
}
|
||||
.add-card-btn {
|
||||
font-size: 1em;
|
||||
font-weight: 400;
|
||||
color: #3e525b;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
background-color: transparent;
|
||||
border-style: dashed;
|
||||
border-color: rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.cursor-text {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.bade-component {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
padding: calc(0.25rem - 1px) 0.25rem;
|
||||
height: 1.25rem;
|
||||
border: 1px solid transparent;
|
||||
min-width: 1.25rem;
|
||||
font-weight: 600;
|
||||
font-size: .625rem;
|
||||
letter-spacing: .04em;
|
||||
text-transform: uppercase;
|
||||
vertical-align: bottom;
|
||||
border-radius: 4px;
|
||||
}
|
||||
// sso-helper-page
|
||||
.sso-helper-container{
|
||||
width: 60vw;
|
||||
|
|
|
|||