[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>
This commit is contained in:
Arpit 2022-06-14 11:06:36 +05:30 committed by GitHub
parent e1b3c26e1d
commit b48fd53ec2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1277 additions and 1 deletions

100
docs/docs/widgets/kanban.md Normal file
View 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'}}>
![ToolJet - Kanban widget](/img/widgets/kanban/kanban.png)
</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'}}>
![ToolJet - Kanban widget](/img/widgets/kanban/kanban-events.png)
</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.
:::
![ToolJet - Kanban widget](/img/widgets/kanban/properties.png)
</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'}}>
![ToolJet - Kanban widget](/img/widgets/kanban/layout.png)
</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'}}>
![ToolJet - List view widget](/img/widgets/kanban/styles.png)
</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'}}>
![ToolJet - List view widget](/img/widgets/kanban/variables.png)
</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}}` |

View file

@ -115,6 +115,7 @@ const sidebars = {
'widgets/file-picker',
'widgets/iframe',
'widgets/image',
'widgets/kanban',
'widgets/listview',
'widgets/map',
'widgets/modal',

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View 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

View 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

View file

@ -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=",

View file

@ -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",

View file

@ -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,
};

View 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;

View 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>
);
};

View 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>
);
};

View 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;

View 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>
);
};

View 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));
};

View file

@ -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);

View file

@ -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' },
},
},
},
];

View file

@ -493,6 +493,11 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
'onCalendarViewChange',
'onSearchTextChanged',
'onPageChange',
'onCardAdded',
'onCardRemoved',
'onCardMoved',
'onCardSelected',
'onCardUpdated',
'onTabSwitch',
].includes(eventName)
) {

View file

@ -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;