Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a task progress indicator to notes in board view #897

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
Expand Down
10 changes: 10 additions & 0 deletions src/lib/stores/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,12 @@
"check": "Check",
"none": "None"
},
"tooltips": {
"checklist-items": "Checklist items",
"task-points": "Story points",
"checked-total": "Cards checked/total",
"task-points-total": "Story point total"
},
"unprioritized": "Unprioritized",
"include-fields": "Include fields",
"settings": {
Expand All @@ -308,6 +314,10 @@
"order-sync-field": {
"name": "Sync card order with field",
"description": "Field to store the position of cards in the board."
},
"points-field": {
"name": "Card story points field",
"description": "Use the selected field as the card story point value."
}
},
"note": {
Expand Down
18 changes: 18 additions & 0 deletions src/ui/components/Indicator/Indicator.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts">
import { Icon } from "obsidian-svelte";
export let icon: string = "";
export let tooltip: string = "";
</script>

<div class="task-indicator" aria-label={tooltip}>
<Icon name={icon} size="sm" />
<slot />
</div>

<style>
div.task-indicator {
display: flex;
gap: 4px;
align-items: center;
}
</style>
1 change: 1 addition & 0 deletions src/ui/views/Board/BoardView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@
)}
{columnWidth}
checkField={fields.find((field) => field.name === config?.checkField)?.name}
pointsField={fields.find((field) => field.name === config?.pointsField)?.name}
includeFields={fields.filter((field) => includeFields.includes(field.name))}
customHeader={fields.find((field) => field.name === customHeader)}
onRecordClick={handleRecordClick}
Expand Down
2 changes: 2 additions & 0 deletions src/ui/views/Board/components/Board/Board.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
export let onColumnCollapse: OnColumnCollapse;
export let onColumnPin: OnColumnPin;
export let checkField: string | undefined;
export let pointsField: string | undefined;
export let includeFields: DataField[];
export let customHeader: DataField | undefined;

Expand Down Expand Up @@ -82,6 +83,7 @@
records={column.records}
{onRecordClick}
{checkField}
{pointsField}
{onRecordCheck}
onRecordAdd={() => onRecordAdd(column.id)}
onDrop={(record, records, trigger) => {
Expand Down
7 changes: 7 additions & 0 deletions src/ui/views/Board/components/Board/BoardColumn.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
export let readonly: boolean;
export let richText: boolean;
export let checkField: string | undefined;
export let pointsField: string | undefined;
export let includeFields: DataField[];
export let customHeader: DataField | undefined;
export let pinned: boolean;
Expand All @@ -42,6 +43,9 @@

$: count = records.length;
$: checkedCount = records.filter((r) => r.values[checkField ?? ""]).length;
$: pointsCount = records.map((r) => r.values[pointsField ?? ""] as number)
.filter(Number.isFinite)
.reduce((sum, p) => sum + p, 0);

function onColumnMenu() {
const menu = new Menu();
Expand Down Expand Up @@ -109,10 +113,12 @@
value={name}
{count}
{checkedCount}
{pointsCount}
bind:editing
{richText}
{collapse}
{checkField}
{pointsField}
{onColumnMenu}
{onColumnRename}
{onValidate}
Expand All @@ -125,6 +131,7 @@
{customHeader}
{onRecordClick}
{checkField}
{pointsField}
{onRecordCheck}
{onDrop}
{includeFields}
Expand Down
29 changes: 28 additions & 1 deletion src/ui/views/Board/components/Board/CardList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
// import { Checkbox, InternalLink} from "obsidian-svelte";
import { Checkbox } from "obsidian-svelte";
import InternalLink from "src/ui/components/InternalLink.svelte";

import {
isString,
type DataField,
type DataRecord,
} from "src/lib/dataframe/dataframe";
import { app } from "src/lib/stores/obsidian";
import { settings } from "src/lib/stores/settings";
import { i18n } from "src/lib/stores/i18n";
import { get } from "svelte/store";
import CardMetadata from "src/ui/components/CardMetadata/CardMetadata.svelte";
import ColorItem from "src/ui/components/ColorItem/ColorItem.svelte";
import Indicator from "src/ui/components/Indicator/Indicator.svelte";
import {
getRecordColorContext,
handleHoverLink,
Expand All @@ -24,6 +26,7 @@
} from "svelte-dnd-action";
import { flip } from "svelte/animate";
import { getDisplayName } from "./boardHelpers";
import { getTaskProgress } from "./taskHelpers";
import type {
DropTrigger,
OnRecordClick,
Expand All @@ -39,6 +42,9 @@
export let checkField: string | undefined;
const checked = (item: DataRecord): boolean =>
checkField ? (item.values[checkField] as boolean) : false;
export let pointsField: string | undefined;
const getTaskPoints = (item: DataRecord) =>
pointsField ? (item.values[pointsField] as number) : null;
export let customHeader: DataField | undefined;
export let boardEditing: boolean;

Expand Down Expand Up @@ -90,6 +96,8 @@
>
{#each items as item (item.id)}
{@const color = getRecordColor(item)}
{@const taskPoints = getTaskPoints(item)}
{@const taskProgress = getTaskProgress(item.id)}

<article
class="projects--board--card"
Expand Down Expand Up @@ -141,6 +149,18 @@
{/if}
</div>
<CardMetadata fields={includeFields} record={item} />
<div class="task-indicators">
{#if taskProgress}
<Indicator icon="check-square" tooltip={get(i18n).t("views.board.tooltips.checklist-items")}>
{taskProgress}
</Indicator>
{/if}
{#if taskPoints}
<Indicator icon="weight" tooltip={get(i18n).t("views.board.tooltips.task-points")}>
{taskPoints}
</Indicator>
{/if}
</div>
</ColorItem>
</article>
{/each}
Expand All @@ -153,6 +173,13 @@
align-items: center;
}

div.task-indicators {
display: flex;
gap: 4px;
align-items: center;
gap: 10px;
}

.checkbox-wrapper {
display: flex;
flex-direction: column;
Expand Down
15 changes: 13 additions & 2 deletions src/ui/views/Board/components/Board/ColumnHeader.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<script lang="ts">
import { MarkdownRenderer, Menu } from "obsidian";
import { app, view } from "src/lib/stores/obsidian";
import { i18n } from "src/lib/stores/i18n";
import { get } from "svelte/store";
import { getContext } from "svelte";
import { TextInput, IconButton } from "obsidian-svelte";
import { TextInput, IconButton, Icon } from "obsidian-svelte";
import { Flair } from "src/ui/components/Flair";
import { handleHoverLink } from "src/ui/views/helpers";

export let value: string;
export let count: number;
export let checkedCount: number;
export let checkField: string | undefined;
export let pointsCount: number;
export let pointsField: string | undefined;
export let collapse: boolean = false;
export let richText: boolean = false;
const sourcePath = getContext<string>("sourcePath") ?? "";
Expand Down Expand Up @@ -126,8 +130,15 @@
</span>
{/if}
<div>
{#if pointsField && pointsCount}
<Flair variant="primary" tooltip={get(i18n).t("views.board.tooltips.task-points-total")}>
<Icon name="weight" size="xs" --icon-color="var(--text-color)"/>
{pointsCount}
</Flair>
{/if}
{#if collapse || checkField}
<Flair variant="primary">
<Flair variant="primary" tooltip={get(i18n).t("views.board.tooltips.checked-total")}>
<Icon name="wallet-cards" size="xs" --icon-color="var(--text-color)"/>
{checkField ? `${checkedCount}/${count}` : count}
</Flair>
{/if}
Expand Down
17 changes: 17 additions & 0 deletions src/ui/views/Board/components/Board/taskHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { app } from "src/lib/stores/obsidian";
import { get } from "svelte/store";

export function getTaskProgress(recordId: string): string {
let progress = "";

const totalTasks = get(app)
.metadataCache.getCache(recordId)
?.listItems?.filter((item) => item.task !== undefined);

if (totalTasks?.length) {
const completedTasks = totalTasks?.filter((item) => item.task !== " ");
progress = `${completedTasks.length}/${totalTasks.length}`;
}

return progress;
}
22 changes: 19 additions & 3 deletions src/ui/views/Board/settings/BoardSettings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
let columnWidthValue = config.columnWidth ?? null;

$: headerField = config.headerField ?? "";

$: orderSyncField = config.orderSyncField ?? "";
$: validOrderSyncFields = getFieldsByType(fields, DataFieldType.Number);
$: pointsField = config.pointsField ?? "";

$: numberFields = getFieldsByType(fields, DataFieldType.Number);

const updateConfig = <T extends keyof BoardConfig>(
key: T,
Expand Down Expand Up @@ -69,7 +70,7 @@
>
<Select
value={orderSyncField ?? ""}
options={validOrderSyncFields.map(fieldToSelectableValue)}
options={numberFields.map(fieldToSelectableValue)}
placeholder={$i18n.t("views.board.fields.none") ?? ""}
allowEmpty
on:change={(event) => {
Expand All @@ -78,5 +79,20 @@
}}
/>
</SettingItem>
<SettingItem
name={$i18n.t("views.board.settings.points-field.name")}
description={$i18n.t("views.board.settings.points-field.description")}
>
<Select
value={pointsField ?? ""}
options={numberFields.map(fieldToSelectableValue)}
placeholder={$i18n.t("views.board.fields.none") ?? ""}
allowEmpty
on:change={(event) => {
pointsField = event.detail;
updateConfig("pointsField", pointsField);
}}
/>
</SettingItem>
</ModalContent>
</ModalLayout>
1 change: 1 addition & 0 deletions src/ui/views/Board/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export interface BoardConfig {
readonly checkField?: string;
readonly headerField?: string;
readonly orderSyncField?: string;
readonly pointsField?: string;
readonly columnWidth?: number;
readonly columns?: ColumnSettings;
readonly includeFields?: string[];
Expand Down
Loading