Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.

Commit 7097bf5

Browse files
authored
feat: new multiple suggester for subfolder rules (#453)
closes #445
1 parent c56a73e commit 7097bf5

File tree

4 files changed

+146
-42
lines changed

4 files changed

+146
-42
lines changed

src/services/FileGroupingService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,9 @@ export class FileGroupingService {
6464
ddbbConfig: LocalSettings,
6565
): Promise<number> {
6666
try {
67-
if (!ddbbConfig.group_folder_column) return 0;
6867
if (!ddbbConfig.automatically_group_files) return 0;
6968
let numberOfMovedFiles = 0;
70-
const pathColumns: string[] = ddbbConfig.group_folder_column
69+
const pathColumns: string[] = (ddbbConfig.group_folder_column || '')
7170
.split(",")
7271
.filter(Boolean);
7372
for (const row of rows) {
@@ -137,6 +136,7 @@ export class FileGroupingService {
137136
directory: string,
138137
ddbbConfig: LocalSettings,
139138
) {
139+
if(!ddbbConfig.automatically_group_files) return;
140140
if (!ddbbConfig.remove_empty_folders) return;
141141
try {
142142
const removedDirectories =
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { DatabaseView } from "DatabaseView";
2+
import { ButtonComponent, SearchComponent, Setting } from "obsidian";
3+
import { FileAttributeSuggester } from "settings/suggesters/FileAttributeSuggester";
4+
5+
export class FileGroupingColumnsSetting {
6+
fileAttributeSuggester: FileAttributeSuggester;
7+
labelContainer: HTMLParagraphElement;
8+
searchComponent: SearchComponent;
9+
debouncedOrganizeNotesIntoSubfolders: any;
10+
label: HTMLSpanElement;
11+
12+
constructor(
13+
private view: DatabaseView,
14+
private allowedColumns: Set<string>,
15+
private organize: () => Promise<void>,
16+
) {}
17+
18+
init = (containerEl: HTMLElement) => {
19+
new Setting(containerEl)
20+
.setName("Columns to group files by")
21+
.setDesc("The folder structure will mirror the values of these columns")
22+
.addSearch((sc) => {
23+
sc.setPlaceholder("").setValue("").onChange(this.onSearchChange);
24+
this.searchComponent = sc;
25+
const currentlySelectedColumns = new Set(
26+
this.view.diskConfig.yaml.config.group_folder_column.split(","),
27+
);
28+
this.fileAttributeSuggester = new FileAttributeSuggester(
29+
this.searchComponent.inputEl,
30+
[...this.allowedColumns].filter(
31+
(c) => !currentlySelectedColumns.has(c),
32+
),
33+
);
34+
})
35+
.addButton(this.onClearButtonClick);
36+
37+
this.configureDisplay(containerEl);
38+
};
39+
40+
onSearchChange = (value: string) => {
41+
if (value && !this.allowedColumns.has(value)) return;
42+
43+
const previouslySelectedColumns =
44+
this.view.diskConfig.yaml.config.group_folder_column
45+
.split(",")
46+
.filter(Boolean);
47+
48+
const newSeting = new Set(
49+
[...previouslySelectedColumns, value].filter(Boolean),
50+
);
51+
52+
this.view.diskConfig.updateConfig({
53+
group_folder_column: [...newSeting].join(","),
54+
});
55+
56+
this.organize();
57+
this.searchComponent.clearButtonEl.click();
58+
this.searchComponent.inputEl.blur();
59+
this.fileAttributeSuggester.options = [...this.allowedColumns].filter(
60+
(v) => !newSeting.has(v),
61+
);
62+
this.renderLabel([...newSeting]);
63+
};
64+
65+
private renderLabel = (values: string[]) => {
66+
if (values.filter(Boolean).length) {
67+
this.label.innerHTML =
68+
values
69+
.map((v) => `<span style='color: #ccc;'>${v}</span>`)
70+
.join("<span style='color: #666;'> / </span>") || "None";
71+
this.labelContainer.style.display = "flex";
72+
} else {
73+
this.labelContainer.style.display = "none";
74+
}
75+
};
76+
77+
private onClearButtonClick = (button: ButtonComponent) => {
78+
button.setButtonText("Reset");
79+
button.onClick(async () => {
80+
this.label.innerHTML = "None";
81+
this.labelContainer.style.display = "none";
82+
this.view.diskConfig.updateConfig({ group_folder_column: "" });
83+
this.searchComponent.clearButtonEl.click();
84+
this.searchComponent.inputEl.blur();
85+
this.fileAttributeSuggester.options = [...this.allowedColumns];
86+
});
87+
};
88+
89+
90+
private configureDisplay = (containerEl: HTMLElement) => {
91+
this.labelContainer = containerEl.createEl("div");
92+
const label = containerEl.createEl("span", {
93+
text: "Selected columns: ",
94+
});
95+
label.style.color = "#666";
96+
this.labelContainer.appendChild(label);
97+
98+
this.label = containerEl.createEl("span");
99+
this.labelContainer.appendChild(this.label);
100+
this.labelContainer.style.gap = "15px";
101+
this.labelContainer.style.marginBottom = "20px";
102+
this.renderLabel(
103+
this.view.diskConfig.yaml.config.group_folder_column.split(","),
104+
);
105+
};
106+
}

src/settings/handlers/columns/GroupFolderColumnTextInputHandler.ts

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { InputType } from "helpers/Constants";
22
import { destination_folder } from "helpers/FileManagement";
3-
import { Notice } from "obsidian";
43
import { FileGroupingService } from "services/FileGroupingService";
54
import { AbstractSettingsHandler, SettingHandlerResponse } from "settings/handlers/AbstractSettingHandler";
6-
import { add_text, add_toggle } from "settings/SettingsComponents";
5+
import { add_toggle } from "settings/SettingsComponents";
6+
import { FileGroupingColumnsSetting } from "./FileGroupingColumnsSetting";
77

88
const createDebouncer = ( callback: (...args: any[])=> void,
99
debounceDelay: number,) =>{
@@ -31,53 +31,21 @@ export class GroupFolderColumnTextInputHandler extends AbstractSettingsHandler {
3131
.filter( (f) => columns[f].input === InputType.SELECT)
3232
.map((key) => columns[key].label),
3333
);
34-
const lowerCaseAllowedColumns = new Set( Array.from(allowedColumns).map((f) => f.toLowerCase()),);
35-
const lowerCaseAllowedColumnsMap = new Map( Array.from(allowedColumns).map((key) => [key.toLowerCase(), key]),)
36-
const debouncedNotice = createDebouncer((message, messageDelay)=>new Notice(message, messageDelay), 1500);
37-
const debouncedOrganizeNotesIntoSubfolders = createDebouncer(async ()=>{
34+
35+
36+
const debouncedOrganizeNotesIntoSubfolders = createDebouncer(async ()=>{
3837
const folderPath = destination_folder(view, view.diskConfig.yaml.config);
3938
await FileGroupingService.organizeNotesIntoSubfolders( folderPath, view.rows, view.diskConfig.yaml.config );
4039
await FileGroupingService.removeEmptyFolders(folderPath, view.diskConfig.yaml.config);
4140
}, 5000);
42-
const group_folder_column_input_promise =
43-
async ( value: string ): Promise<void> => {
44-
45-
const validConfig =
46-
value === "" ||
47-
value
48-
.split(",")
49-
.every((column) => lowerCaseAllowedColumns.has( column.toLowerCase()));
41+
42+
new FileGroupingColumnsSetting(view, allowedColumns,debouncedOrganizeNotesIntoSubfolders.debounce as any).init(containerEl);
5043

51-
if (validConfig){
52-
debouncedNotice.cleanup();
53-
debouncedOrganizeNotesIntoSubfolders.cleanup();
54-
// make sure the case of each column is correct
55-
const correctCaseColumns = value
56-
.split(",")
57-
.map((column) => lowerCaseAllowedColumnsMap.get(column.toLowerCase()))
58-
.join(",");
59-
view.diskConfig.updateConfig({ group_folder_column: correctCaseColumns });
60-
debouncedOrganizeNotesIntoSubfolders.debounce();
61-
}
62-
else {
63-
debouncedNotice.debounce(`"${value}" is an invalid value for group_folder_column`, 4000)
64-
}
65-
};
6644

67-
add_text(
68-
containerEl,
69-
this.settingTitle,
70-
"Multiple columns can be used, separated by a comma. Available columns: " +
71-
[...allowedColumns].join(", "),
72-
"Comma separated column names",
73-
view.diskConfig.yaml.config
74-
.group_folder_column,
75-
group_folder_column_input_promise,
76-
);
7745

7846
add_toggle(
7947
containerEl,
80-
"Automatically group all files into folders",
48+
"Group all files into folders automatically",
8149
"By default, files are groupped individually, after a value is updated",
8250
view.diskConfig.yaml.config.automatically_group_files,
8351
async (value) => {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
2+
import { TextInputSuggest } from "settings/suggesters/suggest";
3+
4+
export class FileAttributeSuggester extends TextInputSuggest<string> {
5+
6+
constructor(inputEl: HTMLInputElement, private _options: string[]) {
7+
super(inputEl);
8+
}
9+
10+
set options(options: string[]) {
11+
this._options = options;
12+
}
13+
getSuggestions(inputStr: string): string[] {
14+
15+
const lowerCaseinputStr = inputStr.toLowerCase()
16+
return this._options.filter((option) => {
17+
return option.toLowerCase().includes(lowerCaseinputStr);
18+
});
19+
}
20+
21+
renderSuggestion(value: string, el: HTMLElement): void {
22+
el.setText(value);
23+
}
24+
25+
selectSuggestion(value: string): void {
26+
this.inputEl.value = value;
27+
this.inputEl.trigger("input");
28+
this.close();
29+
}
30+
}

0 commit comments

Comments
 (0)