Skip to content
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
18 changes: 9 additions & 9 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"id": "obsidian-kanban",
"name": "Kanban",
"version": "2.0.51",
"minAppVersion": "1.0.0",
"description": "Create markdown-backed Kanban boards in Obsidian.",
"author": "mgmeyers",
"authorUrl": "https://github.com/mgmeyers/obsidian-kanban",
"helpUrl": "https://publish.obsidian.md/kanban/Obsidian+Kanban+Plugin",
"isDesktopOnly": false
"id": "obsidian-kanban-am",
"name": "Kanban AM",
"version": "2.0.51",
"minAppVersion": "1.0.0",
"description": "Create markdown-backed Kanban boards in Obsidian (AM fork).",
"author": "mgmeyers and lPhiNix",
"authorUrl": "https://github.com/lPhiNix/obsidian-kanban",
"helpUrl": "https://publish.obsidian.md/kanban/Obsidian+Kanban+Plugin",
"isDesktopOnly": false
}
93 changes: 93 additions & 0 deletions src/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export interface KanbanSettings {
'move-dates'?: boolean;
'move-tags'?: boolean;
'move-task-metadata'?: boolean;
'auto-create-note'?: boolean;
'auto-create-note-counter'?: number;
'auto-create-note-name-format'?: string;
'new-card-insertion-method'?: 'prepend' | 'prepend-compact' | 'append';
'new-line-trigger'?: 'enter' | 'shift-enter';
'new-note-folder'?: string;
Expand Down Expand Up @@ -120,6 +123,9 @@ export const settingKeyLookup: Set<keyof KanbanSettings> = new Set([
'move-dates',
'move-tags',
'move-task-metadata',
'auto-create-note',
'auto-create-note-counter',
'auto-create-note-name-format',
'new-card-insertion-method',
'new-line-trigger',
'new-note-folder',
Expand Down Expand Up @@ -470,6 +476,93 @@ export class SettingsManager {
})
);

new Setting(contentEl)
.setName(t('Auto-create note on card add'))
.setDesc(
t(
'When enabled, adding a card will automatically create a linked note (BOARD-ID format) instead of a plain text card.'
)
)
.then((setting) => {
let toggleComponent: ToggleComponent;

setting
.addToggle((toggle) => {
toggleComponent = toggle;

const [value, globalValue] = this.getSetting('auto-create-note', local);

if (value !== undefined) {
toggle.setValue(value as boolean);
} else if (globalValue !== undefined) {
toggle.setValue(globalValue as boolean);
} else {
toggle.setValue(false);
}

toggle.onChange((newValue) => {
this.applySettingsUpdate({
'auto-create-note': {
$set: newValue,
},
});
});
})
.addExtraButton((b) => {
b.setIcon('lucide-rotate-ccw')
.setTooltip(t('Reset to default'))
.onClick(() => {
const [, globalValue] = this.getSetting('auto-create-note', local);
toggleComponent.setValue(!!(globalValue ?? false));

this.applySettingsUpdate({
$unset: ['auto-create-note'],
});
});
});
});

new Setting(contentEl)
.setName(t('Auto-create note name format'))
.setDesc(
t(
'Template for the note name when auto-creating notes. Available tokens: {{board}}, {{board_lower}}, {{board_raw}}, {{id}}, {{millis}}, {{date}}, {{time}}, {{random}}'
)
)
.addText((text) => {
const [value, globalValue] = this.getSetting('auto-create-note-name-format', local);

text.setPlaceholder((globalValue as string) || '{{board}}-{{id}}');
text.setValue((value as string) || '');

text.onChange((newValue) => {
if (newValue) {
this.applySettingsUpdate({
'auto-create-note-name-format': { $set: newValue },
});
} else {
this.applySettingsUpdate({
$unset: ['auto-create-note-name-format'],
});
}
});
})
.addExtraButton((b) => {
b.setIcon('lucide-rotate-ccw')
.setTooltip(t('Reset to default'))
.onClick(() => {
const [, globalValue] = this.getSetting('auto-create-note-name-format', local);
const el = b.extraSettingsEl
.closest('.setting-item')
?.querySelector('input') as HTMLInputElement | null;
if (el) el.value = '';

this.applySettingsUpdate({
$unset: ['auto-create-note-name-format'],
});
});
});

contentEl.createEl('h4', { text: t('Tags') });

new Setting(contentEl)
Expand Down
17 changes: 15 additions & 2 deletions src/components/Item/ItemForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ interface ItemFormProps {
editState: EditState;
setEditState: Dispatch<StateUpdater<EditState>>;
hideButton?: boolean;
onAutoCreate?: () => void;
}

export function ItemForm({ addItems, editState, setEditState, hideButton }: ItemFormProps) {
export function ItemForm({
addItems,
editState,
setEditState,
hideButton,
onAutoCreate,
}: ItemFormProps) {
const { stateManager } = useContext(KanbanContext);
const editorRef = useRef<EditorView>();

Expand Down Expand Up @@ -70,7 +77,13 @@ export function ItemForm({ addItems, editState, setEditState, hideButton }: Item
<div className={c('item-button-wrapper')}>
<button
className={c('new-item-button')}
onClick={() => setEditState({ x: 0, y: 0 })}
onClick={() => {
if (onAutoCreate) {
onAutoCreate();
} else {
setEditState({ x: 0, y: 0 });
}
}}
onDragOver={(e) => {
if (getDropAction(stateManager, e.dataTransfer)) {
setEditState({ x: 0, y: 0 });
Expand Down
53 changes: 51 additions & 2 deletions src/components/Lane/Lane.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import animateScrollTo from 'animated-scroll-to';
import classcat from 'classcat';
import update from 'immutability-helper';
import { TFile, TFolder } from 'obsidian';
import { Fragment, memo, useCallback, useContext, useMemo, useRef, useState } from 'preact/compat';
import {
DraggableProps,
Expand All @@ -18,7 +19,7 @@ import { getTaskStatusDone } from 'src/parsers/helpers/inlineMetadata';
import { Items } from '../Item/Item';
import { ItemForm } from '../Item/ItemForm';
import { KanbanContext, SearchContext, SortContext } from '../context';
import { c, generateInstanceId } from '../helpers';
import { applyTemplate, c, generateInstanceId, resolveNoteNameFormat } from '../helpers';
import { DataTypes, EditState, EditingState, Item, Lane } from '../types';
import { LaneHeader } from './LaneHeader';

Expand Down Expand Up @@ -117,6 +118,47 @@ function DraggableLaneRaw({
[boardModifiers, path, lane, shouldPrepend]
);

const createAutoNote = useCallback(async () => {
const currentCounter =
(stateManager.state.data.settings?.['auto-create-note-counter'] as number) ?? 0;
const nextCounter = currentCounter + 1;

const nameFormat =
(stateManager.getSetting('auto-create-note-name-format') as string) || '{{board}}-{{id}}';
const noteName = resolveNoteNameFormat(nameFormat, stateManager, nextCounter);

const newNoteFolder = stateManager.getSetting('new-note-folder');
const newNoteTemplatePath = stateManager.getSetting('new-note-template');

const targetFolder = newNoteFolder
? (stateManager.app.vault.getAbstractFileByPath(newNoteFolder as string) as TFolder)
: stateManager.app.fileManager.getNewFileParent(stateManager.file.path);

stateManager.setState((board) =>
update(board, {
data: { settings: { 'auto-create-note-counter': { $set: nextCounter } } },
})
);

const newFile = (await (stateManager.app.fileManager as any).createNewMarkdownFile(
targetFolder,
noteName
)) as TFile;

const newLeaf = stateManager.app.workspace.splitActiveLeaf();
await newLeaf.openFile(newFile);
stateManager.app.workspace.setActiveLeaf(newLeaf, false, true);

await applyTemplate(stateManager, newNoteTemplatePath as string | undefined);

const wikilink = stateManager.app.fileManager.generateMarkdownLink(
newFile,
stateManager.file.path
);

addItems([stateManager.getNewItem(wikilink, ' ')]);
}, [stateManager, addItems]);

const DroppableComponent = isStatic ? StaticDroppable : Droppable;
const SortableComponent = isStatic ? StaticSortable : Sortable;
const CollapsedDropArea = !isCollapsed || isStatic ? Fragment : Droppable;
Expand Down Expand Up @@ -162,6 +204,7 @@ function DraggableLaneRaw({
laneIndex={laneIndex}
lane={lane}
setIsItemInputVisible={isCompactPrepend ? setEditState : undefined}
onAutoCreate={isCompactPrepend ? createAutoNote : undefined}
isCollapsed={isCollapsed}
toggleIsCollapsed={toggleIsCollapsed}
/>
Expand All @@ -172,6 +215,7 @@ function DraggableLaneRaw({
hideButton={isCompactPrepend}
editState={editState}
setEditState={setEditState}
onAutoCreate={createAutoNote}
/>
)}

Expand Down Expand Up @@ -207,7 +251,12 @@ function DraggableLaneRaw({
)}

{!search?.query && !isCollapsed && !shouldPrepend && (
<ItemForm addItems={addItems} editState={editState} setEditState={setEditState} />
<ItemForm
addItems={addItems}
editState={editState}
setEditState={setEditState}
onAutoCreate={createAutoNote}
/>
)}
</CollapsedDropArea>
</div>
Expand Down
13 changes: 12 additions & 1 deletion src/components/Lane/LaneHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface LaneHeaderProps {
laneIndex: number;
bindHandle: (el: HTMLElement) => void;
setIsItemInputVisible?: Dispatch<StateUpdater<EditState>>;
onAutoCreate?: () => void;
isCollapsed: boolean;
toggleIsCollapsed: () => void;
}
Expand All @@ -30,13 +31,15 @@ interface LaneButtonProps {
editState: EditState;
setEditState: Dispatch<StateUpdater<EditState>>;
setIsItemInputVisible?: Dispatch<StateUpdater<EditState>>;
onAutoCreate?: () => void;
}

function LaneButtons({
settingsMenu,
editState,
setEditState,
setIsItemInputVisible,
onAutoCreate,
}: LaneButtonProps) {
const { stateManager } = useContext(KanbanContext);
return (
Expand All @@ -55,7 +58,13 @@ function LaneButtons({
<a
aria-label={t('Add a card')}
className={`${c('lane-settings-button')} clickable-icon`}
onClick={() => setIsItemInputVisible({ x: 0, y: 0 })}
onClick={() => {
if (onAutoCreate) {
onAutoCreate();
} else {
setIsItemInputVisible({ x: 0, y: 0 });
}
}}
onDragOver={(e) => {
if (getDropAction(stateManager, e.dataTransfer)) {
setIsItemInputVisible({ x: 0, y: 0 });
Expand Down Expand Up @@ -85,6 +94,7 @@ export const LaneHeader = memo(function LaneHeader({
laneIndex,
bindHandle,
setIsItemInputVisible,
onAutoCreate,
isCollapsed,
toggleIsCollapsed,
}: LaneHeaderProps) {
Expand Down Expand Up @@ -161,6 +171,7 @@ export const LaneHeader = memo(function LaneHeader({
editState={editState}
setEditState={setEditState}
setIsItemInputVisible={setIsItemInputVisible}
onAutoCreate={onAutoCreate}
settingsMenu={settingsMenu}
/>
</div>
Expand Down
40 changes: 40 additions & 0 deletions src/components/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,46 @@ export function generateInstanceId(len: number = 9): string {
.slice(2, 2 + len);
}

export const DEFAULT_NOTE_NAME_FORMAT = '{{board}}-{{id}}';

/**
* Resolves a note name format string, replacing tokens with actual values.
*
* Available tokens:
* {{board}} — board name, uppercased, spaces → "-"
* {{board_lower}} — board name, lowercased, spaces → "-"
* {{board_raw}} — board name as-is, spaces → "-"
* {{id}} — autoincremental counter (persisted in board settings)
* {{millis}} — current timestamp in milliseconds
* {{date}} — current date using the board's date-format setting
* {{time}} — current time using the board's time-format setting
* {{random}} — short random alphanumeric string (6 chars)
*/
export function resolveNoteNameFormat(
format: string,
stateManager: StateManager,
nextCounter: number
): string {
const basename = stateManager.file.basename;
const boardUpper = basename.toUpperCase().replace(/\s+/g, '-');
const boardLower = basename.toLowerCase().replace(/\s+/g, '-');
const boardRaw = basename.replace(/\s+/g, '-');

const dateFormat = (stateManager.getSetting('date-format') as string) || 'YYYY-MM-DD';
const timeFormat = (stateManager.getSetting('time-format') as string) || 'HH:mm';
const now = moment();

return format
.replace(/\{\{board\}\}/g, boardUpper)
.replace(/\{\{board_lower\}\}/g, boardLower)
.replace(/\{\{board_raw\}\}/g, boardRaw)
.replace(/\{\{id\}\}/g, String(nextCounter))
.replace(/\{\{millis\}\}/g, String(Date.now()))
.replace(/\{\{date\}\}/g, now.format(dateFormat))
.replace(/\{\{time\}\}/g, now.format(timeFormat))
.replace(/\{\{random\}\}/g, generateInstanceId(6));
}

export function maybeCompleteForMove(
sourceStateManager: StateManager,
sourceBoard: Board,
Expand Down
6 changes: 6 additions & 0 deletions src/lang/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ const en = {
'Card title...': 'Card title...',
'Add card': 'Add card',
'Add a card': 'Add a card',
'Auto-create note on card add': 'Auto-create note on card add',
'When enabled, adding a card will automatically create a linked note (BOARD-ID format) instead of a plain text card.':
'When enabled, adding a card will automatically create a linked note (BOARD-ID format) instead of a plain text card.',
'Auto-create note name format': 'Auto-create note name format',
'Template for the note name when auto-creating notes. Available tokens: {{board}}, {{board_lower}}, {{board_raw}}, {{id}}, {{millis}}, {{date}}, {{time}}, {{random}}':
'Template for the note name when auto-creating notes. Available tokens: {{board}}, {{board_lower}}, {{board_raw}}, {{id}}, {{millis}}, {{date}}, {{time}}, {{random}}',

// components/Item/ItemMenu.ts
'Edit card': 'Edit card',
Expand Down