Merge pull request #2 from OlegLustenko/feature/tags-rename
add find-by tag and to a directory functionality
This commit is contained in:
commit
327e25c998
|
@ -27,3 +27,4 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
npm run test
|
||||||
|
|
33
README.md
33
README.md
|
@ -11,8 +11,39 @@ Now you can rename a bunch of files from the directory and all references also w
|
||||||
|
|
||||||
> Under the hood this plugin is using obsidian API for renaming, but we just applying it for many files
|
> Under the hood this plugin is using obsidian API for renaming, but we just applying it for many files
|
||||||
|
|
||||||
## How to use?
|
# Features
|
||||||
|
|
||||||
|
> Whenever we're updating **Replacement Symbols** you can set new _Directory Location_ too
|
||||||
|
> so, you can also move files to _another directory_
|
||||||
|
|
||||||
|
|
||||||
|
## Rename/Move files based on folder location
|
||||||
|
Click _Search By Folder_
|
||||||
|
|
||||||
|
Update **Folder Location** where are you wanting to get files from, put **Existing Characters** from the file path
|
||||||
|
later on update **Replacement Symbols** those symbols will be used for in a new path.
|
||||||
|
|
||||||
|
|
||||||
|
## Rename/Move files based on tags
|
||||||
|
Click _Search By Tags_
|
||||||
|
|
||||||
|
Put tags in **Tags names** field search will be completed across the vault, use coma separator if you need more than 1 tag
|
||||||
|
Click Refresh
|
||||||
|
Update **Existing Characters** field with a pattern you are looking for in existing notes, set **Replacement Symbols**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Click Preview or `Enter` to see intermediate results(nothing will be changed/moved/renamed).
|
||||||
|
|
||||||
|
Click `Rename` whenever you done
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Update files based on tags
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## API
|
||||||
- **folder location** - Files from which folder you need to rename
|
- **folder location** - Files from which folder you need to rename
|
||||||
- **Symbols in existing files** - the symbols/characters that we have in the files
|
- **Symbols in existing files** - the symbols/characters that we have in the files
|
||||||
- **Replacement Symbols** - a new symbols that will be pasted instead
|
- **Replacement Symbols** - a new symbols that will be pasted instead
|
||||||
|
|
57
main.ts
57
main.ts
|
@ -2,12 +2,7 @@ import { App, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
|
||||||
|
|
||||||
import { FolderSuggest } from './src/suggestions/folderSuggest';
|
import { FolderSuggest } from './src/suggestions/folderSuggest';
|
||||||
import { renderDonateButton } from './src/components/DonateButton';
|
import { renderDonateButton } from './src/components/DonateButton';
|
||||||
import {
|
import { renameFilesInObsidian } from './src/services/file.service';
|
||||||
getFilesNamesInDirectory,
|
|
||||||
getRenderedFileNamesReplaced,
|
|
||||||
renameFilesInObsidian,
|
|
||||||
syncScrolls,
|
|
||||||
} from './src/services/file.service';
|
|
||||||
import { createPreviewElement } from './src/components/PreviewElement';
|
import { createPreviewElement } from './src/components/PreviewElement';
|
||||||
import {
|
import {
|
||||||
getObsidianFilesByFolderName,
|
getObsidianFilesByFolderName,
|
||||||
|
@ -83,6 +78,13 @@ export class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
const { containerEl } = this;
|
const { containerEl } = this;
|
||||||
containerEl.empty();
|
containerEl.empty();
|
||||||
containerEl.createEl('h2', { text: 'Bulk Rename - Settings' });
|
containerEl.createEl('h2', { text: 'Bulk Rename - Settings' });
|
||||||
|
containerEl.addEventListener('keyup', (event) => {
|
||||||
|
if (event.key !== 'Enter') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reRenderPreview();
|
||||||
|
});
|
||||||
|
|
||||||
this.renderTabs();
|
this.renderTabs();
|
||||||
this.renderFileLocation();
|
this.renderFileLocation();
|
||||||
|
@ -95,7 +97,7 @@ export class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
|
|
||||||
renderTabs() {
|
renderTabs() {
|
||||||
new Setting(this.containerEl)
|
new Setting(this.containerEl)
|
||||||
.setName('toggle view')
|
.setName('UI will be changed when you click those buttons')
|
||||||
.addButton((button) => {
|
.addButton((button) => {
|
||||||
button.setButtonText('Search by folder');
|
button.setButtonText('Search by folder');
|
||||||
if (isViewTypeFolder(this.plugin)) {
|
if (isViewTypeFolder(this.plugin)) {
|
||||||
|
@ -134,17 +136,14 @@ export class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
.onChange((newFolder) => {
|
.onChange((newFolder) => {
|
||||||
this.plugin.settings.folderName = newFolder;
|
this.plugin.settings.folderName = newFolder;
|
||||||
this.plugin.saveSettings();
|
this.plugin.saveSettings();
|
||||||
this.calculateFiles();
|
this.getFilesByFolder();
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
cb.containerEl.addClass('templater_search');
|
cb.containerEl.addClass('templater_search');
|
||||||
})
|
})
|
||||||
.addButton((button) => {
|
.addButton((button) => {
|
||||||
button.setButtonText('Refresh');
|
button.setButtonText('Refresh');
|
||||||
button.onClick(() => {
|
button.onClick(this.reRenderPreview);
|
||||||
this.calculateFiles();
|
|
||||||
this.reRenderPreview();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,8 +164,6 @@ export class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
|
|
||||||
this.plugin.settings.tags = target.value.replace(/ /g, '').split(',');
|
this.plugin.settings.tags = target.value.replace(/ /g, '').split(',');
|
||||||
this.plugin.saveSettings();
|
this.plugin.saveSettings();
|
||||||
this.getFilesByTags();
|
|
||||||
this.reRenderPreview();
|
|
||||||
});
|
});
|
||||||
cb.setPlaceholder('Example: #tag, #tag2')
|
cb.setPlaceholder('Example: #tag, #tag2')
|
||||||
.setValue(this.plugin.settings.tags.join(','))
|
.setValue(this.plugin.settings.tags.join(','))
|
||||||
|
@ -180,10 +177,7 @@ export class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
})
|
})
|
||||||
.addButton((button) => {
|
.addButton((button) => {
|
||||||
button.setButtonText('Refresh');
|
button.setButtonText('Refresh');
|
||||||
button.onClick(() => {
|
button.onClick(this.reRenderPreview);
|
||||||
this.getFilesByTags();
|
|
||||||
this.reRenderPreview();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,10 +196,9 @@ export class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
desc.appendChild(button);
|
desc.appendChild(button);
|
||||||
|
|
||||||
const newSettings = new Setting(this.containerEl)
|
const newSettings = new Setting(this.containerEl)
|
||||||
.setName('Existing Symbol')
|
.setName('Existing Characters')
|
||||||
.setDesc(desc);
|
.setDesc(desc);
|
||||||
|
|
||||||
// if (!isViewTypeTags(this.plugin)) {
|
|
||||||
newSettings.addText((textComponent) => {
|
newSettings.addText((textComponent) => {
|
||||||
textComponent.setValue(settings.existingSymbol);
|
textComponent.setValue(settings.existingSymbol);
|
||||||
textComponent.setPlaceholder('existing symbols');
|
textComponent.setPlaceholder('existing symbols');
|
||||||
|
@ -214,7 +207,7 @@ export class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
this.plugin.saveSettings();
|
this.plugin.saveSettings();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// }
|
|
||||||
newSettings.addText((textComponent) => {
|
newSettings.addText((textComponent) => {
|
||||||
const previewLabel = createPreviewElement('Replacement symbols');
|
const previewLabel = createPreviewElement('Replacement symbols');
|
||||||
textComponent.inputEl.insertAdjacentElement('beforebegin', previewLabel);
|
textComponent.inputEl.insertAdjacentElement('beforebegin', previewLabel);
|
||||||
|
@ -223,7 +216,7 @@ export class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
textComponent.onChange((newValue) => {
|
textComponent.onChange((newValue) => {
|
||||||
settings.replacePattern = newValue;
|
settings.replacePattern = newValue;
|
||||||
this.plugin.saveSettings();
|
this.plugin.saveSettings();
|
||||||
this.calculateFiles();
|
this.getFilesByFolder();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -232,6 +225,7 @@ export class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
this.filesAndPreview = new Setting(this.containerEl)
|
this.filesAndPreview = new Setting(this.containerEl)
|
||||||
.setName('Files within the folder')
|
.setName('Files within the folder')
|
||||||
.setDesc(`Total Files: ${this.plugin.settings.fileNames.length}`);
|
.setDesc(`Total Files: ${this.plugin.settings.fileNames.length}`);
|
||||||
|
this.calculateFileNames();
|
||||||
|
|
||||||
renderPreviewFiles(this.filesAndPreview, this.plugin, this.state);
|
renderPreviewFiles(this.filesAndPreview, this.plugin, this.state);
|
||||||
};
|
};
|
||||||
|
@ -268,7 +262,20 @@ export class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
renderDonateButton(this.containerEl);
|
renderDonateButton(this.containerEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateFiles() {
|
reRenderPreview = () => {
|
||||||
|
this.calculateFileNames();
|
||||||
|
renderPreviewFiles(this.filesAndPreview, this.plugin, this.state);
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateFileNames() {
|
||||||
|
if (isViewTypeTags(this.plugin)) {
|
||||||
|
this.getFilesByTags();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.getFilesByFolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilesByFolder() {
|
||||||
this.plugin.settings.fileNames = getObsidianFilesByFolderName(
|
this.plugin.settings.fileNames = getObsidianFilesByFolderName(
|
||||||
this.app,
|
this.app,
|
||||||
this.plugin,
|
this.plugin,
|
||||||
|
@ -281,10 +288,6 @@ export class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
this.plugin,
|
this.plugin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
reRenderPreview = () => {
|
|
||||||
renderPreviewFiles(this.filesAndPreview, this.plugin, this.state);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BulkRenamePlugin;
|
export default BulkRenamePlugin;
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import {
|
import {
|
||||||
getFilesNamesInDirectory,
|
getFilesNamesInDirectory,
|
||||||
getRenderedFileNamesReplaced,
|
getRenderedFileNamesReplaced,
|
||||||
syncScrolls,
|
|
||||||
} from '../services/file.service';
|
} from '../services/file.service';
|
||||||
import { createPreviewElement } from './PreviewElement';
|
import { createPreviewElement } from './PreviewElement';
|
||||||
import BulkRenamePlugin, { BulkRenameSettingsTab } from '../../main';
|
import BulkRenamePlugin, { BulkRenameSettingsTab, State } from '../../main';
|
||||||
|
|
||||||
export const renderPreviewFiles = (
|
export const renderPreviewFiles = (
|
||||||
setting: BulkRenameSettingsTab['filesAndPreview'],
|
setting: BulkRenameSettingsTab['filesAndPreview'],
|
||||||
|
@ -43,3 +42,24 @@ export const renderPreviewFiles = (
|
||||||
syncScrolls(existingFilesTextArea, replacedPreviewTextArea, state);
|
syncScrolls(existingFilesTextArea, replacedPreviewTextArea, state);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const syncScrolls = (
|
||||||
|
existingFilesArea: HTMLTextAreaElement,
|
||||||
|
previewArea: HTMLTextAreaElement,
|
||||||
|
state: State,
|
||||||
|
) => {
|
||||||
|
existingFilesArea.addEventListener('scroll', (event) => {
|
||||||
|
const target = event.target as HTMLTextAreaElement;
|
||||||
|
if (target.scrollTop !== state.previewScroll) {
|
||||||
|
previewArea.scrollTop = target.scrollTop;
|
||||||
|
state.previewScroll = target.scrollTop;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
previewArea.addEventListener('scroll', (event) => {
|
||||||
|
const target = event.target as HTMLTextAreaElement;
|
||||||
|
if (target.scrollTop !== state.filesScroll) {
|
||||||
|
existingFilesArea.scrollTop = target.scrollTop;
|
||||||
|
state.filesScroll = target.scrollTop;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { App, Notice, TFile } from 'obsidian';
|
import { App, Notice, TFile } from 'obsidian';
|
||||||
import BulkRenamePlugin, { State } from '../../main';
|
import BulkRenamePlugin from '../../main';
|
||||||
|
|
||||||
export const getFilesNamesInDirectory = (plugin: BulkRenamePlugin) => {
|
export const getFilesNamesInDirectory = (plugin: BulkRenamePlugin) => {
|
||||||
const { fileNames } = plugin.settings;
|
const { fileNames } = plugin.settings;
|
||||||
|
@ -26,7 +26,7 @@ export const getRenderedFileNamesReplaced = (plugin: BulkRenamePlugin) => {
|
||||||
return getFilesAsString(newFiles);
|
return getFilesAsString(newFiles);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectFilenamesWithReplacedPath = (plugin: BulkRenamePlugin) => {
|
export const selectFilenamesWithReplacedPath = (plugin: BulkRenamePlugin) => {
|
||||||
const { fileNames } = plugin.settings;
|
const { fileNames } = plugin.settings;
|
||||||
|
|
||||||
return fileNames.map((file) => {
|
return fileNames.map((file) => {
|
||||||
|
@ -42,10 +42,11 @@ export const replaceFilePath = (plugin: BulkRenamePlugin, file: TFile) => {
|
||||||
|
|
||||||
const pathWithoutExtension = file.path.split('.').slice(0, -1).join('.');
|
const pathWithoutExtension = file.path.split('.').slice(0, -1).join('.');
|
||||||
|
|
||||||
const newPath = pathWithoutExtension?.replaceAll(
|
const convertedToRegExpString = escapeRegExp(existingSymbol);
|
||||||
existingSymbol,
|
|
||||||
replacePattern,
|
const regExpSymbol = new RegExp(convertedToRegExpString, 'g');
|
||||||
);
|
|
||||||
|
const newPath = pathWithoutExtension?.replace(regExpSymbol, replacePattern);
|
||||||
|
|
||||||
return `${newPath}.${file.extension}`;
|
return `${newPath}.${file.extension}`;
|
||||||
};
|
};
|
||||||
|
@ -61,11 +62,6 @@ export const renameFilesInObsidian = async (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (replacePattern === existingSymbol) {
|
|
||||||
// new Notice("Replace Pattern shouldn't much Existing Symbol");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!fileNames.length) {
|
if (!fileNames.length) {
|
||||||
new Notice('Please check your results before rename!');
|
new Notice('Please check your results before rename!');
|
||||||
return;
|
return;
|
||||||
|
@ -81,23 +77,9 @@ export const renameFilesInObsidian = async (
|
||||||
new Notice('successfully renamed all files');
|
new Notice('successfully renamed all files');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncScrolls = (
|
let reRegExpChar = /[\\^$.*+?()[\]{}]/g,
|
||||||
existingFilesArea: HTMLTextAreaElement,
|
reHasRegExpChar = RegExp(reRegExpChar.source);
|
||||||
previewArea: HTMLTextAreaElement,
|
|
||||||
state: State,
|
export function escapeRegExp(s: string) {
|
||||||
) => {
|
return s && reHasRegExpChar.test(s) ? s.replace(reRegExpChar, '\\$&') : s;
|
||||||
existingFilesArea.addEventListener('scroll', (event) => {
|
}
|
||||||
const target = event.target as HTMLTextAreaElement;
|
|
||||||
if (target.scrollTop !== state.previewScroll) {
|
|
||||||
previewArea.scrollTop = target.scrollTop;
|
|
||||||
state.previewScroll = target.scrollTop;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
previewArea.addEventListener('scroll', (event) => {
|
|
||||||
const target = event.target as HTMLTextAreaElement;
|
|
||||||
if (target.scrollTop !== state.filesScroll) {
|
|
||||||
existingFilesArea.scrollTop = target.scrollTop;
|
|
||||||
state.filesScroll = target.scrollTop;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { TFile } from 'obsidian';
|
import { TFile } from 'obsidian';
|
||||||
|
|
||||||
import { replaceFilePath } from './file.service';
|
import {
|
||||||
|
replaceFilePath,
|
||||||
|
selectFilenamesWithReplacedPath,
|
||||||
|
} from './file.service';
|
||||||
import BulkRenamePlugin from '../../main';
|
import BulkRenamePlugin from '../../main';
|
||||||
|
|
||||||
describe('File Services', () => {
|
describe('File Services', () => {
|
||||||
|
@ -79,5 +82,66 @@ describe('File Services', () => {
|
||||||
|
|
||||||
expect(result).toEqual(expectedResult);
|
expect(result).toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('selectFilenamesWithReplacedPath', () => {
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
path: 'journals/2022_10_13.md',
|
||||||
|
extension: 'md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pages/2022_10_13.md',
|
||||||
|
extension: 'md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bulkRenameTets/2022_10_13.md',
|
||||||
|
extension: 'md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'YesWecan/canWe/2022_10_13.md',
|
||||||
|
extension: 'md',
|
||||||
|
},
|
||||||
|
] as unknown as TFile[];
|
||||||
|
|
||||||
|
const mockPluginPlugin = {
|
||||||
|
settings: {
|
||||||
|
fileNames: files,
|
||||||
|
},
|
||||||
|
} as unknown as BulkRenamePlugin;
|
||||||
|
|
||||||
|
it('should rename many files with RegExp', () => {
|
||||||
|
const plugin = {
|
||||||
|
...mockPluginPlugin,
|
||||||
|
settings: {
|
||||||
|
...mockPluginPlugin.settings,
|
||||||
|
existingSymbol: 'journals|pages|bulkRenameTets|canWe',
|
||||||
|
replacePattern: 'qwe',
|
||||||
|
},
|
||||||
|
} as unknown as BulkRenamePlugin;
|
||||||
|
|
||||||
|
const expectedResults = [
|
||||||
|
{
|
||||||
|
path: 'qwe/2022_10_13.md',
|
||||||
|
extension: 'md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'qwe/2022_10_13.md',
|
||||||
|
extension: 'md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'qwe/2022_10_13.md',
|
||||||
|
extension: 'md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'YesWecan/qwe/2022_10_13.md',
|
||||||
|
extension: 'md',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedFiles = selectFilenamesWithReplacedPath(plugin);
|
||||||
|
|
||||||
|
expect(expectedResults).toEqual(updatedFiles);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
describe('obsidian.service', () => {
|
||||||
|
describe('getObsidianFilesWithTagName', () => {
|
||||||
|
it.todo('should find files by tag');
|
||||||
|
});
|
||||||
|
});
|
|
@ -8,11 +8,11 @@ export const getObsidianFilesByFolderName = (
|
||||||
const { folderName } = plugin.settings;
|
const { folderName } = plugin.settings;
|
||||||
const abstractFiles = app.vault.getAllLoadedFiles();
|
const abstractFiles = app.vault.getAllLoadedFiles();
|
||||||
|
|
||||||
const files = abstractFiles.filter((file) => {
|
const files = abstractFiles.filter(
|
||||||
return file instanceof TFile && file.parent.name.includes(folderName);
|
(file) => file instanceof TFile && file.parent.name.includes(folderName),
|
||||||
});
|
) as TFile[];
|
||||||
|
|
||||||
const filesSortedByName = files.sort((a, b) => a.name.localeCompare(b.name));
|
const filesSortedByName = sortFilesByName(files);
|
||||||
|
|
||||||
return filesSortedByName;
|
return filesSortedByName;
|
||||||
};
|
};
|
||||||
|
@ -28,12 +28,9 @@ export const getObsidianFilesWithTagName = (
|
||||||
if (!(file instanceof TFile)) {
|
if (!(file instanceof TFile)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fileMetadata = app.metadataCache.getFileCache(file);
|
|
||||||
if (!fileMetadata) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fileMetadata.tags) {
|
const fileMetadata = app.metadataCache.getFileCache(file);
|
||||||
|
if (!fileMetadata || !fileMetadata.tags) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,9 +43,13 @@ export const getObsidianFilesWithTagName = (
|
||||||
}
|
}
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
});
|
}) as TFile[];
|
||||||
|
|
||||||
const filesSortedByName = files.sort((a, b) => a.name.localeCompare(b.name));
|
const filesSortedByName = sortFilesByName(files);
|
||||||
|
|
||||||
return filesSortedByName;
|
return filesSortedByName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sortFilesByName = (files: TFile[]) => {
|
||||||
|
return files.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue