Add relations, custom fields and card customisation

This commit is contained in:
Gordon 2021-06-07 20:42:35 +01:00
parent 95b87052b4
commit 0aa5776af3
9 changed files with 290 additions and 113 deletions

View File

@ -1,8 +1,10 @@
# 0.11.0
* Added KaTeX support to task markdown (description and comments)
* Added KaTeX support to task markdown (description and comments) using `$$...$$` for blocks and `$...$` for inline
* Added syntax highlighting to code blocks in task markdown (description and comments)
* Added column sorting
* Added relations and custom fields to task cards
* Added task card customisation using `board.css`
# 0.10.0

View File

@ -8,6 +8,62 @@ Visual Studio Code will add `vscode-light`, `vscode-dark` and `vscode-high-contr
Various Codicon icons have been used in this extension. Check [here](https://code.visualstudio.com/api/references/icons-in-labels) for a listing of available icons.
## Column and tag configuration
Each column is given a CSS class containing the column name in param-case. You can use this to apply custom styles to specific columns.
For example, say we have a column called 'In Progress'. We can apply a custom border colour like so:
```css
.kanbn-column-in-progress .kanbn-column-task-list {
border-color: #ff0;
}
```
Similarly, each tag is given a CSS class containing the tag name in param-case.
For example, say we have tags called 'QA required' and 'Breaking Change'. We can apply custom styles like so:
```css
.kanbn-task-tag-qa-required {
background-color: #418;
}
.kanbn-task-tag-breaking-change {
background-color: #900;
color: #eee;
font-size: 1.5em;
}
```
## Task card configuration
The task cards that appear on the Kanbn board can be configured by showing or hiding different elements, or by applying alternative styles to them.
The following elements are hidden by default (but can be re-enabled by adding a custom board style):
- Updated date
- Started date
- Due date
- Completed date
- Number of comments
- Relations
Task fields are generally contained within a `.kanbn-task-data` element, most of which have `display: inline-block` by default. Additionally, the outer-container of a task card is given CSS classes containing the task's column name, completed status and overdue status.
Here's an example of a task card style using some of the above features:
```css
.kanbn-task-data-relation {
display: block; /* this will display task relations, one per line */
}
.kanbn-task-data-relation-blocks {
color: #900;
font-weight: bold; /* this will highlight task relations of type 'blocks' */
}
.kanbn-task-completed .kanbn-task-data-completed {
display: inline-block; /* this will show the completed date only for tasks that have been completed */
}
```
## CSS classes
### Kanbn board
@ -24,7 +80,7 @@ Various Codicon icons have been used in this extension. Check [here](https://cod
- `kanbn-header-description`
- `kanbn-board`
- `kanbn-column`
- `kanbn-column-{Column name in snake-case}`
- `kanbn-column-{Column name in param-case}`
- `kanbn-column-name`
- `kanbn-column-count`
- `kanbn-column-button`
@ -35,19 +91,30 @@ Various Codicon icons have been used in this extension. Check [here](https://cod
- `kanbn-column-task-list`
- `kanbn-column-task-list.drag-over`
- `kanbn-task`
- `kanbn-task-row`
- `kanbn-task-name`
- `kanbn-task-data`
- `kanbn-task-tags`
- `kanbn-task-tag`
- `kanbn-task-tag-{Tag name in snake-case}`
- `kanbn-task-assigned`
- `kanbn-task-date`
- `kanbn-task-workload`
- `kanbn-task-progress`
- `kanbn-task-{Column name in param-case}`
- `kanbn-task-overdue`
- `kanbn-task-comments`
- `kanbn-task-sub-tasks`
- `kanbn-task-completed`
- `drag`
- `kanbn-task-data`
- `kanbn-task-data-label`
- `kanbn-task-data-name`
- `kanbn-task-data-tags`
- `kanbn-task-tag`
- `kanbn-task-tag-{Tag name in param-case}`
- `kanbn-task-data-custom-field`
- `kanbn-task-data-{Custom field name in param-case}
- `kanbn-task-data-assigned`
- `kanbn-task-data-created`
- `kanbn-task-data-updated`
- `kanbn-task-data-started`
- `kanbn-task-data-due`
- `kanbn-task-data-completed`
- `kanbn-task-data-comments`
- `kanbn-task-data-sub-tasks`
- `kanbn-task-data-relation`
- `kanbn-task-data-relation-{Relation type in param-case}`
- `kanbn-task-data-workload`
- `kanbn-task-progress`
### Task editor
@ -107,7 +174,7 @@ Various Codicon icons have been used in this extension. Check [here](https://cod
- `kanbn-task-editor-row-tag`
- `kanbn-task-editor-field-tag`
- `kanbn-task-editor-tag-highlight`
- `kanbn-task-tag-{Tag name in snake-case}`
- `kanbn-task-tag-{Tag name in param-case}`
### Burndown chart

View File

@ -79,6 +79,7 @@ export default class KanbnBoardPanel {
startedColumns: index.options.startedColumns ?? [],
completedColumns: index.options.completedColumns ?? [],
columnSorting: index.options.columnSorting ?? {},
customFields: index.options.customFields ?? [],
dateFormat: KanbnBoardPanel.currentPanel._kanbn.getDateFormat(index),
showBurndownButton: vscode.workspace.getConfiguration("kanbn").get("showBurndownButton"),
showSprintButton: vscode.workspace.getConfiguration("kanbn").get("showSprintButton"),

24
package-lock.json generated
View File

@ -1352,9 +1352,9 @@
}
},
"@basementuniverse/kanbn": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@basementuniverse/kanbn/-/kanbn-0.9.0.tgz",
"integrity": "sha512-LYJlJ6/KLkxqW/ZpPkLsAYTZ6ki5wmP9ZIgLPNW28+KVXhFz78/+3amoynMeOFUOBiKymCJYlKtvly6y0CvdTg==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@basementuniverse/kanbn/-/kanbn-0.10.0.tgz",
"integrity": "sha512-inNRbX1ZuiUiub0PbEDJAZGI9m92+AwGWnoByEZEHAq0L8rD1Z8DZVzeyXQDhrNdt8gpkjINR8+l8NJ8Tmqc0Q==",
"requires": {
"asciichart": "^1.5.25",
"auto-load": "^3.0.4",
@ -4168,9 +4168,9 @@
"dev": true
},
"chrono-node": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.2.6.tgz",
"integrity": "sha512-ahgxpY4ihg3frV5t7pZYrS0Iap5MErTQ7whVNBxbiLjplc2HhGwj3zgr0dEnJos/FAuZVjrHoky8J9YiNc5ZKQ==",
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.2.7.tgz",
"integrity": "sha512-QQxlQUwebgNJDtUJpRQlhKoJJgSfvNTe6Zk1LgyVHVnbCj6xKygmiviJonS/n5A4xd9qjEl1u+ke2vzD77ty/g==",
"requires": {
"dayjs": "^1.10.0"
}
@ -10245,9 +10245,9 @@
}
},
"humanize-duration": {
"version": "3.26.0",
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.26.0.tgz",
"integrity": "sha512-SddekX3p5ApvPY6bbAYppGKe874jP6iFZXYtrQToDV4R0j2UpTYPqwTFM2QpXpuw9DhS/eXTUnKYTF9TbXAJ6A=="
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.0.tgz",
"integrity": "sha512-qLo/08cNc3Tb0uD7jK0jAcU5cnqCM0n568918E7R2XhMr/+7F37p4EY062W/stg7tmzvknNn9b/1+UhVRzsYrQ=="
},
"iconv-lite": {
"version": "0.4.24",
@ -13798,9 +13798,9 @@
"dev": true
},
"nextgen-events": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/nextgen-events/-/nextgen-events-1.4.0.tgz",
"integrity": "sha512-bwSpShfuLg/lcRFokZvtUcrn5kumy6uRI6Jc+QWzH+bW6ZRdog7LfwAwhqeTzVfsprZgDGrctr6rO5aJhcdAfg=="
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/nextgen-events/-/nextgen-events-1.5.2.tgz",
"integrity": "sha512-0ZEIRQywH5Oxt2IYYufRltQg/KjXhKM7f7MHve+ZIRaKnIR1PPYEXAl2WBmej5Sf0Qh2GgE/21sMRZVuOyxLzw=="
},
"nice-try": {
"version": "1.0.5",

View File

@ -98,7 +98,7 @@
}
},
"dependencies": {
"@basementuniverse/kanbn": "^0.9.0",
"@basementuniverse/kanbn": "^0.10.0",
"dateformat": "^4.5.1",
"formik": "^2.2.6",
"git-user-name": "^2.0.0",

View File

@ -18,6 +18,7 @@ function App() {
const [startedColumns, setStartedColumns] = useState([]);
const [completedColumns, setCompletedColumns] = useState([]);
const [columnSorting, setColumnSorting] = useState({});
const [customFields, setCustomFields] = useState([]);
const [dateFormat, setDateFormat] = useState('');
const [task, setTask] = useState({});
const [tasks, setTasks] = useState({});
@ -48,6 +49,7 @@ function App() {
setStartedColumns(event.data.startedColumns);
setCompletedColumns(event.data.completedColumns);
setColumnSorting(event.data.columnSorting);
setCustomFields(event.data.customFields);
setShowBurndownButton(event.data.showBurndownButton);
setShowSprintButton(event.data.showSprintButton);
@ -101,6 +103,7 @@ function App() {
startedColumns={startedColumns}
completedColumns={completedColumns}
columnSorting={columnSorting}
customFields={customFields}
dateFormat={dateFormat}
showBurndownButton={showBurndownButton}
showSprintButton={showSprintButton}

View File

@ -140,6 +140,7 @@ const Board = ({
startedColumns,
completedColumns,
columnSorting,
customFields,
dateFormat,
showBurndownButton,
showSprintButton,
@ -153,6 +154,7 @@ const Board = ({
startedColumns: string[],
completedColumns: string[],
columnSorting: { [columnName: string]: { field: string, order: 'ascending' | 'descending' }[] },
customFields: { name: string, type: 'boolean' | 'date' | 'number' | 'string' }[],
dateFormat: string,
showBurndownButton: boolean,
showSprintButton: boolean,
@ -326,6 +328,8 @@ const Board = ({
>
{column.filter(task => filterTask(task, taskFilter)).map((task, position) => <TaskItem
task={task}
columnName={columnName}
customFields={customFields}
position={position}
dateFormat={dateFormat}
vscode={vscode}

View File

@ -4,8 +4,10 @@ import formatDate from 'dateformat';
import { paramCase } from '@basementuniverse/kanbn/src/utility';
import VSCodeApi from "./VSCodeApi";
const TaskItem = ({ task, position, dateFormat, vscode }: {
const TaskItem = ({ task, columnName, customFields, position, dateFormat, vscode }: {
task: KanbnTask,
columnName: string,
customFields: { name: string, type: 'boolean' | 'date' | 'number' | 'string' }[],
position: number,
dateFormat: string,
vscode: VSCodeApi
@ -38,6 +40,9 @@ const TaskItem = ({ task, position, dateFormat, vscode }: {
{...provided.dragHandleProps}
className={[
'kanbn-task',
`kanbn-task-column-${paramCase(columnName)}`,
checkOverdue(task) ? 'kanbn-task-overdue' : null,
!!completedDate ? 'kanbn-task-completed' : null,
snapshot.isDragging ? 'drag' : null
].filter(i => i).join(' ')}
style={{
@ -45,10 +50,9 @@ const TaskItem = ({ task, position, dateFormat, vscode }: {
...provided.draggableProps.style
}}
>
<div className="kanbn-task-row">
<div className="kanbn-task-data kanbn-task-data-name">
<button
type="button"
className="kanbn-task-name"
onClick={() => {
vscode.postMessage({
command: 'kanbn.task',
@ -61,11 +65,10 @@ const TaskItem = ({ task, position, dateFormat, vscode }: {
{task.name}
</button>
</div>
<div className="kanbn-task-row">
{
'tags' in task.metadata &&
task.metadata.tags!.length > 0 &&
<div className="kanbn-task-data kanbn-task-tags">
<div className="kanbn-task-data kanbn-task-data-tags">
{task.metadata.tags!.map(tag => {
return (
<span className={[
@ -78,51 +81,108 @@ const TaskItem = ({ task, position, dateFormat, vscode }: {
})}
</div>
}
{
customFields.map(customField => {
if (customField.name in task.metadata) {
return (
<div className={[
'kanbn-task-data kanbn-task-data-custom-field',
`kanbn-task-data-${paramCase(customField.name)}`
].join(' ')}>
{
customField.type === 'boolean'
? (
<>
<i className={`codicon codicon-${task.metadata[customField.name]
? 'pass-filled'
: 'circle-large-outline'}`}></i>
{customField.name}
</>
) : (
<>
<i className="codicon codicon-json"></i>
<span title={customField.name}>
{task.metadata[customField.name]}
</span>
</>
)
}
</div>
<div className="kanbn-task-row">
);
}
return (<></>);
})
}
{
'assigned' in task.metadata &&
!!task.metadata.assigned &&
<div className="kanbn-task-data kanbn-task-assigned">
<div className="kanbn-task-data kanbn-task-data-assigned">
<i className="codicon codicon-account"></i>{task.metadata.assigned}
</div>
}
{
(createdDate || updatedDate) &&
<div className="kanbn-task-data kanbn-task-date" title={[
createdDate ? `Created ${createdDate}` : null,
updatedDate ? `Updated ${updatedDate}` : null,
startedDate ? `Started ${startedDate}` : null,
dueDate ? `Due ${dueDate}` : null,
completedDate ? `Completed ${completedDate}` : null
].filter(i => i).join('\n')}>
<i
className={[
'codicon codicon-clock',
checkOverdue(task) ? 'kanbn-task-overdue' : null
].filter(i => i).join(' ')}
></i>{updatedDate || createdDate}
createdDate &&
<div className="kanbn-task-data kanbn-task-data-created" title={`Created ${createdDate}`}>
<i className="codicon codicon-clock"></i>{createdDate}
</div>
}
{
updatedDate &&
<div className="kanbn-task-data kanbn-task-data-updated" title={`Updated ${updatedDate}`}>
<i className="codicon codicon-clock"></i>{updatedDate}
</div>
}
{
startedDate &&
<div className="kanbn-task-data kanbn-task-data-started" title={`Started ${startedDate}`}>
<i className="codicon codicon-run"></i>{startedDate}
</div>
}
{
dueDate &&
<div className="kanbn-task-data kanbn-task-data-due" title={`Due ${dueDate}`}>
<i className="codicon codicon-watch"></i>{dueDate}
</div>
}
{
completedDate &&
<div className="kanbn-task-data kanbn-task-data-completed" title={`Completed ${completedDate}`}>
<i className="codicon codicon-check"></i>{completedDate}
</div>
}
{
task.comments.length > 0 &&
<div className="kanbn-task-data kanbn-task-comments">
<div className="kanbn-task-data kanbn-task-data-comments">
<i className="codicon codicon-comment"></i>{task.comments.length}
</div>
}
{
task.subTasks.length > 0 &&
<div className="kanbn-task-data kanbn-task-sub-tasks">
<div className="kanbn-task-data kanbn-task-data-sub-tasks">
<i className="codicon codicon-tasklist"></i>
{task.subTasks.filter(subTask => subTask.completed).length} / {task.subTasks.length}
</div>
}
{
task.workload !== undefined &&
<div className="kanbn-task-data kanbn-task-workload">
<div className="kanbn-task-data kanbn-task-data-workload">
<i className="codicon codicon-run"></i>{task.workload}
</div>
}
{
task.relations.length > 0 &&
task.relations.map(relation => (
<div className={[
'kanbn-task-data kanbn-task-data-relation',
relation.type ? `kanbn-task-data-relation-${relation.type}` : null,
].join(' ')}>
<i className="codicon codicon-link"></i>
<span className="kanbn-task-data-label">
{relation.type}
</span> {relation.task}
</div>
))
}
{
task.workload !== undefined &&
task.progress !== undefined &&
@ -131,7 +191,6 @@ const TaskItem = ({ task, position, dateFormat, vscode }: {
}}></div>
}
</div>
</div>
);
}}
</Draggable>

View File

@ -220,7 +220,33 @@ body.vscode-dark .kanbn-task {
border-color: var(--vscode-activityBar-foreground);
}
.kanbn-task-name {
.kanbn-task-data {
display: inline-block;
margin: 4px 8px 4px 0;
min-width: 30%;
opacity: 0.7;
font-size: 0.9em;
font-weight: normal;
font-style: italic;
}
.kanbn-task-data-label {
opacity: 0.4;
}
.kanbn-task-data-name {
display: block;
}
.kanbn-task-data-tags {
display: block;
}
.kanbn-task-data-relation {
display: block;
}
.kanbn-task-data-name button {
margin: 0;
padding: 0;
border: none;
@ -232,22 +258,13 @@ body.vscode-dark .kanbn-task {
text-align: left;
}
.kanbn-task-name:focus, .kanbn-task-name:hover {
.kanbn-task-data-name button:focus,
.kanbn-task-data-name button:hover {
border: none;
outline: none;
text-decoration: underline;
}
.kanbn-task-data {
display: inline-block;
margin: 4px 8px 4px 0;
min-width: 30%;
opacity: 0.7;
font-size: 0.9em;
font-weight: normal;
font-style: italic;
}
.kanbn-task div .codicon {
position: relative;
top: 1px;
@ -291,7 +308,7 @@ body.vscode-dark .kanbn-task {
background-color: #f42;
}
.kanbn-task-overdue {
.kanbn-task-overdue .kanbn-task-data-due {
color: #f22 !important;
}
@ -305,6 +322,30 @@ body.vscode-dark .kanbn-task {
transition: width .5s ease-in-out;
}
.kanbn-task-data-updated {
display: none;
}
.kanbn-task-data-started {
display: none;
}
.kanbn-task-data-due {
display: none;
}
.kanbn-task-data-completed {
display: none;
}
.kanbn-task-data-comments {
display: none;
}
.kanbn-task-data-relation {
display: none;
}
/*-----------------------------------------------------------------------------
Task editor styles
-----------------------------------------------------------------------------*/