From 6bf83b4dfd67f000536bcc03dad54f7326d2e002 Mon Sep 17 00:00:00 2001 From: Nicole Dresselhaus Date: Tue, 2 Sep 2025 15:56:38 +0200 Subject: [PATCH 1/3] initial implementation. docs completely missing. Tooltip not done yet. --- src/Commands/CreateOrEditTaskParser.ts | 3 + src/Layout/TaskLayoutOptions.ts | 2 + src/Renderer/TaskFieldRenderer.ts | 5 +- src/Renderer/TaskLineRenderer.ts | 9 ++ src/Suggestor/Suggestor.ts | 94 +++++++++++++++++++ src/Task/Duration.ts | 52 ++++++++++ src/Task/Task.ts | 25 +++++ src/TaskSerializer/DataviewTaskSerializer.ts | 3 + src/TaskSerializer/DefaultTaskSerializer.ts | 20 ++++ src/TaskSerializer/index.ts | 1 + src/ui/DurationEditor.svelte | 25 +++++ src/ui/EditTask.svelte | 11 +++ src/ui/EditableTask.ts | 21 +++++ tests/Commands/CreateOrEditTaskParser.test.ts | 4 +- .../CustomMatchersForTaskSerializer.ts | 3 + ...date-tasks_all-emojis-emojis.approved.text | 2 +- ...ate-tasks_find-unread-emojis.approved.text | 2 +- tests/Layout/TaskLayout.test.ts | 1 + tests/Layout/TaskLayoutOptions.test.ts | 5 + tests/Query/Query.test.ts | 3 + ...r_tests_fully_populated_task.approved.html | 4 +- ..._populated_task_-_short_mode.approved.html | 4 +- ...e_HTML_Full_task_-_full_mode.approved.html | 4 +- ..._HTML_Full_task_-_short_mode.approved.html | 4 +- tests/Renderer/TaskLineRenderer.test.ts | 10 ++ ...perties.test.task_other_fields.approved.md | 4 +- tests/Scripting/TaskProperties.test.ts | 2 + ...on_options_for_an_empty_task.approved.json | 4 + ...ymbols_show_all_suggested_text.approved.md | 8 ++ ...on_options_for_an_empty_task.approved.json | 4 + ...ymbols_show_all_suggested_text.approved.md | 8 ++ tests/Suggestor/Suggestor.test.ts | 20 ++++ tests/Task/Task.test.ts | 7 ++ .../DataviewTaskSerializer.test.ts | 21 ++++- .../DefaultTaskSerializer.test.ts | 22 +++++ ...ializer_Emojis_tabulate-emojis.approved.md | 1 + tests/TaskSerializer/TaskSerializer.test.ts | 2 + tests/TestingTools/TaskBuilder.test.ts | 2 +- tests/TestingTools/TaskBuilder.ts | 9 ++ ..._tests_should_match_snapshot.approved.html | 9 ++ ...apshot_-_without_access_keys.approved.html | 9 ++ tests/ui/EditTask.test.ts | 4 + tests/ui/EditableTask.test.ts | 7 +- 43 files changed, 448 insertions(+), 12 deletions(-) create mode 100644 src/Task/Duration.ts create mode 100644 src/ui/DurationEditor.svelte diff --git a/src/Commands/CreateOrEditTaskParser.ts b/src/Commands/CreateOrEditTaskParser.ts index 45b91f8472..0f43b1042f 100644 --- a/src/Commands/CreateOrEditTaskParser.ts +++ b/src/Commands/CreateOrEditTaskParser.ts @@ -8,6 +8,7 @@ import { TaskLocation } from '../Task/TaskLocation'; import { getSettings } from '../Config/Settings'; import { GlobalFilter } from '../Config/GlobalFilter'; import { Priority } from '../Task/Priority'; +import { Duration } from '../Task/Duration'; import { TaskRegularExpressions } from '../Task/TaskRegularExpressions'; function getDefaultCreatedDate() { @@ -89,6 +90,7 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta createdDate, startDate: null, scheduledDate: null, + duration: Duration.None, dueDate: null, doneDate: null, cancelledDate: null, @@ -130,6 +132,7 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta createdDate, startDate: null, scheduledDate: null, + duration: Duration.None, dueDate: null, doneDate: null, cancelledDate: null, diff --git a/src/Layout/TaskLayoutOptions.ts b/src/Layout/TaskLayoutOptions.ts index c53b1abb23..fb7b5d0cac 100644 --- a/src/Layout/TaskLayoutOptions.ts +++ b/src/Layout/TaskLayoutOptions.ts @@ -15,6 +15,7 @@ export enum TaskLayoutComponent { CreatedDate = 'createdDate', StartDate = 'startDate', ScheduledDate = 'scheduledDate', + Duration = 'duration', DueDate = 'dueDate', CancelledDate = 'cancelledDate', DoneDate = 'doneDate', @@ -111,6 +112,7 @@ export function parseTaskShowHideOptions(taskLayoutOptions: TaskLayoutOptions, o ['depends on', TaskLayoutComponent.DependsOn], ['done date', TaskLayoutComponent.DoneDate], ['due date', TaskLayoutComponent.DueDate], + ['duration', TaskLayoutComponent.Duration], ['id', TaskLayoutComponent.Id], ['on completion', TaskLayoutComponent.OnCompletion], ['priority', TaskLayoutComponent.Priority], diff --git a/src/Renderer/TaskFieldRenderer.ts b/src/Renderer/TaskFieldRenderer.ts index 4f62f20ad8..3ef289d06e 100644 --- a/src/Renderer/TaskFieldRenderer.ts +++ b/src/Renderer/TaskFieldRenderer.ts @@ -97,7 +97,7 @@ export class TaskFieldHTMLData { // TS2345: Argument of type 'string[] | Moment' is not assignable to parameter of type 'Moment'. // Type 'string[]' is missing the following properties from type 'Moment': format, startOf, endOf, add, and 78 more. if (!Array.isArray(date) && date instanceof window.moment) { - const attributeValue = dateToAttribute(date); + const attributeValue = dateToAttribute(date as Moment); if (attributeValue) { return attributeValue; } @@ -183,6 +183,9 @@ const taskFieldHTMLData: { [c in TaskLayoutComponent]: TaskFieldHTMLData } = { return PriorityTools.priorityNameUsingNormal(task.priority).toLocaleLowerCase(); }), + duration: new TaskFieldHTMLData('task-duration', 'taskDuration', (_component, task) => { + return task.duration.toText(); + }), description: createFieldWithoutDataAttributes('task-description'), recurrenceRule: createFieldWithoutDataAttributes('task-recurring'), onCompletion: createFieldWithoutDataAttributes('task-onCompletion'), diff --git a/src/Renderer/TaskLineRenderer.ts b/src/Renderer/TaskLineRenderer.ts index 0d3d13bff1..c990948d8e 100644 --- a/src/Renderer/TaskLineRenderer.ts +++ b/src/Renderer/TaskLineRenderer.ts @@ -7,6 +7,7 @@ import { TaskLayoutComponent, type TaskLayoutOptions } from '../Layout/TaskLayou import { replaceTaskWithTasks } from '../Obsidian/File'; import { StatusRegistry } from '../Statuses/StatusRegistry'; import { Task } from '../Task/Task'; +import { Duration } from '../Task/Duration'; import { TaskRegularExpressions } from '../Task/TaskRegularExpressions'; import { StatusMenu } from '../ui/Menus/StatusMenu'; import type { AllTaskDateFields } from '../DateTime/DateFieldTypes'; @@ -225,6 +226,10 @@ export class TaskLineRenderer { this.queryLayoutOptions.shortMode, component, ); + // skip empty duration + if (component == TaskLayoutComponent.Duration && task.duration === Duration.None) { + continue; + } if (componentString) { // Create the text span that will hold the rendered component const span = createAndAppendElement('span', parentElement); @@ -281,6 +286,10 @@ export class TaskLineRenderer { if (li.dataset.taskPriority === undefined) { fieldRenderer.addDataAttribute(li, task, TaskLayoutComponent.Priority); } + // Same logic for duration of 0h0m. It will not be rendered, but data-attribute should be present. + if (li.dataset.taskDuration === undefined) { + fieldRenderer.addDataAttribute(li, task, TaskLayoutComponent.Duration); + } } /* diff --git a/src/Suggestor/Suggestor.ts b/src/Suggestor/Suggestor.ts index 798b9b988e..7d48d335d3 100644 --- a/src/Suggestor/Suggestor.ts +++ b/src/Suggestor/Suggestor.ts @@ -4,6 +4,7 @@ import type { Settings } from '../Config/Settings'; import { DateParser } from '../DateTime/DateParser'; import { doAutocomplete } from '../DateTime/DateAbbreviations'; import { Occurrence } from '../Task/Occurrence'; +import { Duration } from '../Task/Duration'; import { Recurrence } from '../Task/Recurrence'; import { type DefaultTaskSerializerSymbols, @@ -80,6 +81,9 @@ export function makeDefaultSuggestionBuilder( // add date suggestions if relevant suggestions = suggestions.concat(addDatesSuggestions(datePrefixRegex, maxGenericSuggestions, parameters)); + // add duration suggestions if relevant + suggestions = suggestions.concat(addDurationValueSuggestions(symbols.durationSymbol, parameters)); + // add recurrence suggestions if relevant suggestions = suggestions.concat(addRecurrenceValueSuggestions(symbols.recurrenceSymbol, parameters)); @@ -152,6 +156,7 @@ function addTaskPropertySuggestions( addField(genericSuggestions, line, symbols.dueDateSymbol, 'due date'); addField(genericSuggestions, line, symbols.startDateSymbol, 'start date'); addField(genericSuggestions, line, symbols.scheduledDateSymbol, 'scheduled date'); + addField(genericSuggestions, line, symbols.durationSymbol, 'duration'); addPrioritySuggestions(genericSuggestions, symbols, parameters); addField(genericSuggestions, line, symbols.recurrenceSymbol, 'recurring (repeat)'); @@ -272,6 +277,95 @@ function dateExtractor(symbol: string, date: string) { return { displayText, appendText }; } +/* + * If the cursor is located in a section that should be followed by a duration description, suggest options + * for what to enter as a duration. + * This has two parts: either generic predefined suggestions, or a single suggestion that is a parsed result + * of what the user is typing. + * Generic predefined suggestions, in turn, also have two options: either filtered (if the user started typing + * something where a duration is expected) or unfiltered + */ +function addDurationValueSuggestions(durationSymbol: string, parameters: SuggestorParameters) { + let genericSuggestions = ['5m', '15m', '1h', '30m', '45m', '2h', '3h']; + const hourSuggestions = ['15m', '30m', '45m']; + + const results: SuggestInfo[] = []; + const durationRegex = new RegExp(`(${durationSymbol})\\s*([0-9hm]*)`, 'ug'); + const durationMatch = matchIfCursorInRegex(durationRegex, parameters); + if (durationMatch && durationMatch.length >= 1) { + const durationPrefix = durationMatch[1]; + let durationString = ''; + if (durationMatch[2]) { + durationString = durationMatch[2]; + } + if (durationString.length > 0) { + // If the text matches a valid duration, suggest logical continuations + let parsedduration = Duration.fromText(durationString); + if (!parsedduration) { + //not a valid duration string => no h/m yet. Suggest finishing with minutes or complete hour! + if (parseInt(durationString, 10) > 0) { + results.push({ + suggestionType: 'match', + displayText: `${durationPrefix} ${durationString}m`, + appendText: `${durationPrefix} ${durationString}m` + parameters.postfix, + insertAt: durationMatch.index, + insertSkip: calculateSkipValueForMatch(durationMatch[0], parameters), + }); + genericSuggestions = genericSuggestions.filter((s) => s != `${durationString}m`); + results.push({ + suggestionType: 'match', + displayText: `${durationPrefix} ${durationString}h`, + appendText: `${durationPrefix} ${durationString}h` + parameters.postfix, + insertAt: durationMatch.index, + insertSkip: calculateSkipValueForMatch(durationMatch[0], parameters), + }); + genericSuggestions = genericSuggestions.filter((s) => s != `${durationString}h`); + } + // also suggest that '2' implies '2h', thus also suggesting continuations like '2h30m' + parsedduration = Duration.fromText(durationString + 'h'); + } + if (parsedduration) { + // special suggestions on finished hour like '123h' + const genText = (sugg: string) => `${durationPrefix} ${parsedduration!.hours}h${sugg}`; + for (const suggestion of hourSuggestions.filter( + // suggestion is either all suggestions or the one with matching prefix + (s) => parsedduration?.minutes == 0 || s.startsWith(parsedduration!.minutes.toString(10)), + )) { + results.push({ + suggestionType: 'match', + displayText: `${genText(suggestion)}`, + appendText: genText(suggestion) + parameters.postfix, + insertAt: durationMatch.index, + insertSkip: calculateSkipValueForMatch(durationMatch[0], parameters), + }); + } + } + } + // Now to generic predefined suggestions. + // If we get a partial match with some of the suggestions (e.g. the user started typing "3"), + // we use that for matches to the generic example-list above (i.e. "3h"). + // Otherwise, we just display the list of suggestions, and either way, truncate them eventually to + // a max number. + // In the case of duration rules, the max number should be small enough to allow users to "escape" + // the mode of writing a duration rule, i.e. we should leave enough space for component suggestions + const maxGenericDurationSuggestions = parameters.settings.autoSuggestMaxItems / 2; + const genericMatches = filterGenericSuggestions( + genericSuggestions, + durationString, + maxGenericDurationSuggestions, + true, + ); + + const extractor = (durationPrefix: string, match: string) => { + const displayText = `${durationPrefix} ${match}`; + const appendText = `${durationPrefix} ${match}`; + return { displayText, appendText }; + }; + constructSuggestions(parameters, durationMatch, genericMatches, extractor, results); + } + + return results; +} /* * If the cursor is located in a section that should be followed by a date (due, start date or scheduled date), * suggest options for what to enter as a date. diff --git a/src/Task/Duration.ts b/src/Task/Duration.ts new file mode 100644 index 0000000000..a84d700d3c --- /dev/null +++ b/src/Task/Duration.ts @@ -0,0 +1,52 @@ +export class Duration { + readonly hours: number; + readonly minutes: number; + + constructor({ hours, minutes }: { hours: number; minutes: number }) { + this.hours = hours; + this.minutes = minutes; + } + + public static readonly None: Duration = new Duration({ hours: 0, minutes: 0 }); + public static readonly valueRegEx: string = '[0-9]+h[0-9]+m?|[0-9]+h|[0-9]+m?'; + + public static fromText(durationText: string): Duration | null { + try { + let match = durationText.match(/^(\d+)h$/i); + if (match != null) { + if (parseInt(match[1], 10) > 0) { + return new Duration({ hours: parseInt(match[1], 10), minutes: 0 }); + } + } + match = durationText.match(/^(\d+)m$/i); + if (match != null) { + if (parseInt(match[1], 10) > 0) { + return new Duration({ hours: 0, minutes: parseInt(match[1], 10) }); + } + } + match = durationText.match(/^(\d+)h(\d+)m?$/i); + if (match != null) { + if (parseInt(match[1], 10) > 0 || parseInt(match[2], 10) > 0) { + return new Duration({ hours: parseInt(match[1], 10), minutes: parseInt(match[2], 10) }); + } + } + return null; + } catch (e) { + // Could not read recurrence rule. User possibly not done typing. + // Print error message, as it is useful if a test file has not set up window.moment + if (e instanceof Error) { + console.log(e.message); + } + } + + return null; + } + + public toText(): string { + if (this.hours == 0 && this.minutes == 0) { + return ''; + } + + return this.hours + 'h' + this.minutes + 'm'; + } +} diff --git a/src/Task/Task.ts b/src/Task/Task.ts index d8c01b1c0d..4796e76e2d 100644 --- a/src/Task/Task.ts +++ b/src/Task/Task.ts @@ -16,6 +16,7 @@ import { Urgency } from './Urgency'; import type { Recurrence } from './Recurrence'; import type { TaskLocation } from './TaskLocation'; import type { Priority } from './Priority'; +import { Duration } from './Duration'; import { TaskRegularExpressions } from './TaskRegularExpressions'; import { OnCompletion, handleOnCompletion } from './OnCompletion'; @@ -49,6 +50,7 @@ export class Task extends ListItem { public readonly createdDate: Moment | null; public readonly startDate: Moment | null; public readonly scheduledDate: Moment | null; + public readonly duration: Duration; public readonly dueDate: Moment | null; public readonly doneDate: Moment | null; public readonly cancelledDate: Moment | null; @@ -78,6 +80,7 @@ export class Task extends ListItem { createdDate, startDate, scheduledDate, + duration, dueDate, doneDate, cancelledDate, @@ -101,6 +104,7 @@ export class Task extends ListItem { createdDate: moment.Moment | null; startDate: moment.Moment | null; scheduledDate: moment.Moment | null; + duration: Duration; dueDate: moment.Moment | null; doneDate: moment.Moment | null; cancelledDate: moment.Moment | null; @@ -133,6 +137,7 @@ export class Task extends ListItem { this.createdDate = createdDate; this.startDate = startDate; this.scheduledDate = scheduledDate; + this.duration = duration; this.dueDate = dueDate; this.doneDate = doneDate; this.cancelledDate = cancelledDate; @@ -653,6 +658,20 @@ export class Task extends ListItem { return new TasksDate(this.scheduledDate); } + /** + * Return {@link hours} from {@link Duration}. + */ + public get durationHours(): number | string { + return this.duration === Duration.None ? '' : this.duration.hours; + } + + /** + * Return {@link minutes} from {@link Duration}. + */ + public get durationMinutes(): number | string { + return this.duration === Duration.None ? '' : this.duration.minutes; + } + /** * Return {@link startDate} as a {@link TasksDate}, so the field names in scripting docs are consistent with the existing search instruction names, and null values are easy to deal with. */ @@ -780,6 +799,7 @@ export class Task extends ListItem { // happens more often than is ideal. let args: Array = [ 'priority', + 'duration', 'blockLink', 'scheduledDateIsInferred', 'id', @@ -819,6 +839,11 @@ export class Task extends ListItem { if (!this.recurrenceIdenticalTo(other)) { return false; } + // Compare duration. Only identical if their textual representation is identical. + // 1h30m may not be semantically equal to 90m. + if (this.duration.toText() != other.duration.toText()) { + return false; + } return this.file.rawFrontmatterIdenticalTo(other.file); } diff --git a/src/TaskSerializer/DataviewTaskSerializer.ts b/src/TaskSerializer/DataviewTaskSerializer.ts index d2a1cb69f5..57161dfccd 100644 --- a/src/TaskSerializer/DataviewTaskSerializer.ts +++ b/src/TaskSerializer/DataviewTaskSerializer.ts @@ -1,6 +1,7 @@ import { TaskLayoutComponent } from '../Layout/TaskLayoutOptions'; import { PriorityTools } from '../lib/PriorityTools'; import type { Priority } from '../Task/Priority'; +import { Duration } from '../Task/Duration'; import type { Task } from '../Task/Task'; import { DefaultTaskSerializer, taskIdRegex, taskIdSequenceRegex } from './DefaultTaskSerializer'; @@ -72,6 +73,7 @@ export const DATAVIEW_SYMBOLS = { startDateSymbol: 'start::', createdDateSymbol: 'created::', scheduledDateSymbol: 'scheduled::', + durationSymbol: 'duration::', dueDateSymbol: 'due::', doneDateSymbol: 'completion::', cancelledDateSymbol: 'cancelled::', @@ -84,6 +86,7 @@ export const DATAVIEW_SYMBOLS = { startDateRegex: toInlineFieldRegex(/start:: *(\d{4}-\d{2}-\d{2})/), createdDateRegex: toInlineFieldRegex(/created:: *(\d{4}-\d{2}-\d{2})/), scheduledDateRegex: toInlineFieldRegex(/scheduled:: *(\d{4}-\d{2}-\d{2})/), + durationRegex: toInlineFieldRegex(new RegExp('duration:: *(' + Duration.valueRegEx + ')')), dueDateRegex: toInlineFieldRegex(/due:: *(\d{4}-\d{2}-\d{2})/), doneDateRegex: toInlineFieldRegex(/completion:: *(\d{4}-\d{2}-\d{2})/), cancelledDateRegex: toInlineFieldRegex(/cancelled:: *(\d{4}-\d{2}-\d{2})/), diff --git a/src/TaskSerializer/DefaultTaskSerializer.ts b/src/TaskSerializer/DefaultTaskSerializer.ts index ee7ea03e82..c9898975c2 100644 --- a/src/TaskSerializer/DefaultTaskSerializer.ts +++ b/src/TaskSerializer/DefaultTaskSerializer.ts @@ -5,6 +5,7 @@ import { Occurrence } from '../Task/Occurrence'; import { Recurrence } from '../Task/Recurrence'; import { Task } from '../Task/Task'; import { Priority } from '../Task/Priority'; +import { Duration } from '../Task/Duration'; import { TaskRegularExpressions } from '../Task/TaskRegularExpressions'; import type { TaskDetails, TaskSerializer } from '.'; @@ -26,6 +27,7 @@ export interface DefaultTaskSerializerSymbols { readonly startDateSymbol: string; readonly createdDateSymbol: string; readonly scheduledDateSymbol: string; + readonly durationSymbol: string; readonly dueDateSymbol: string; readonly doneDateSymbol: string; readonly cancelledDateSymbol: string; @@ -38,6 +40,7 @@ export interface DefaultTaskSerializerSymbols { startDateRegex: RegExp; createdDateRegex: RegExp; scheduledDateRegex: RegExp; + durationRegex: RegExp; dueDateRegex: RegExp; doneDateRegex: RegExp; cancelledDateRegex: RegExp; @@ -87,6 +90,7 @@ export const DEFAULT_SYMBOLS: DefaultTaskSerializerSymbols = { startDateSymbol: 'πŸ›«', createdDateSymbol: 'βž•', scheduledDateSymbol: '⏳', + durationSymbol: '⏱', dueDateSymbol: 'πŸ“…', doneDateSymbol: 'βœ…', cancelledDateSymbol: '❌', @@ -99,6 +103,7 @@ export const DEFAULT_SYMBOLS: DefaultTaskSerializerSymbols = { startDateRegex: dateFieldRegex('πŸ›«'), createdDateRegex: dateFieldRegex('βž•'), scheduledDateRegex: dateFieldRegex('(?:⏳|βŒ›)'), + durationRegex: fieldRegex('⏱', '(' + Duration.valueRegEx + ')'), dueDateRegex: dateFieldRegex('(?:πŸ“…|πŸ“†|πŸ—“)'), doneDateRegex: dateFieldRegex('βœ…'), cancelledDateRegex: dateFieldRegex('❌'), @@ -170,6 +175,7 @@ export class DefaultTaskSerializer implements TaskSerializer { startDateSymbol, createdDateSymbol, scheduledDateSymbol, + durationSymbol, doneDateSymbol, cancelledDateSymbol, recurrenceSymbol, @@ -206,6 +212,9 @@ export class DefaultTaskSerializer implements TaskSerializer { case TaskLayoutComponent.ScheduledDate: if (task.scheduledDateIsInferred) return ''; return symbolAndDateValue(shortMode, scheduledDateSymbol, task.scheduledDate); + case TaskLayoutComponent.Duration: + if (!task.duration) return ''; + return symbolAndStringValue(shortMode, durationSymbol, task.duration.toText()); case TaskLayoutComponent.DoneDate: return symbolAndDateValue(shortMode, doneDateSymbol, task.doneDate); case TaskLayoutComponent.CancelledDate: @@ -274,6 +283,7 @@ export class DefaultTaskSerializer implements TaskSerializer { let priority: Priority = Priority.None; let startDate: Moment | null = null; let scheduledDate: Moment | null = null; + let duration: Duration = Duration.None; let dueDate: Moment | null = null; let doneDate: Moment | null = null; let cancelledDate: Moment | null = null; @@ -329,6 +339,15 @@ export class DefaultTaskSerializer implements TaskSerializer { matched = true; } + const durationMatch = line.match(TaskFormatRegularExpressions.durationRegex); + if (durationMatch !== null) { + line = line.replace(TaskFormatRegularExpressions.durationRegex, '').trim(); + //note: as TaskFormatRegularExpressions.durationRegex is derived from Duration.valueRegEx + //Duration.fromText() should never(!) return null. + duration = Duration.fromText(durationMatch[1].trim())!; + matched = true; + } + const startDateMatch = line.match(TaskFormatRegularExpressions.startDateRegex); if (startDateMatch !== null) { startDate = window.moment(startDateMatch[1], TaskRegularExpressions.dateFormat); @@ -414,6 +433,7 @@ export class DefaultTaskSerializer implements TaskSerializer { startDate, createdDate, scheduledDate, + duration, dueDate, doneDate, cancelledDate, diff --git a/src/TaskSerializer/index.ts b/src/TaskSerializer/index.ts index a658a7f702..76a66da2df 100644 --- a/src/TaskSerializer/index.ts +++ b/src/TaskSerializer/index.ts @@ -17,6 +17,7 @@ export type TaskDetails = Writeable< | 'startDate' | 'createdDate' | 'scheduledDate' + | 'duration' | 'dueDate' | 'doneDate' | 'cancelledDate' diff --git a/src/ui/DurationEditor.svelte b/src/ui/DurationEditor.svelte new file mode 100644 index 0000000000..f185da5873 --- /dev/null +++ b/src/ui/DurationEditor.svelte @@ -0,0 +1,25 @@ + + +