From 5fbb05f083e269a965bb97165fff8441884cd3a5 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Sat, 1 Oct 2022 20:30:18 +0200 Subject: [PATCH 1/6] #431 - Performance improvements for first load --- e2e/src/command.test.ts | 2 - src/commands/Dashboard.ts | 2 +- src/commands/Folders.ts | 2 +- src/commands/Project.ts | 2 +- src/extension.ts | 7 +- src/helpers/DashboardSettings.ts | 17 +- src/listeners/dashboard/PagesListener.ts | 190 ++--------------- src/listeners/dashboard/SettingsListener.ts | 8 +- src/listeners/dashboard/SnippetListener.ts | 4 +- src/services/PagesParser.ts | 219 ++++++++++++++++++++ 10 files changed, 263 insertions(+), 190 deletions(-) create mode 100644 src/services/PagesParser.ts diff --git a/e2e/src/command.test.ts b/e2e/src/command.test.ts index 482c37b2..103afe18 100644 --- a/e2e/src/command.test.ts +++ b/e2e/src/command.test.ts @@ -68,11 +68,9 @@ describe("Initialization testing", function() { async function notificationExists(workbench: Workbench, text: string): Promise { const notifications = await (await (new StatusBar()).openNotificationsCenter()).getNotifications(NotificationType.Info); - console.log(`Notifications:`, notifications.length); for (const notification of notifications) { const message = await notification.getMessage(); - console.log(message) if (message.indexOf(text) >= 0) { return notification; } diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index b99c44ee..d34e682f 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -131,7 +131,7 @@ export class Dashboard { }); SettingsHelper.onConfigChange(() => { - SettingsListener.getSettings(); + SettingsListener.getSettings(true); }); Dashboard.webview.webview.onDidReceiveMessage(async (msg) => { diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index c12977fd..ea39ae10 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -137,7 +137,7 @@ export class Folders { Telemetry.send(TelemetryEvent.registerFolder); - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } } diff --git a/src/commands/Project.ts b/src/commands/Project.ts index 8e708f4c..d7cb3488 100644 --- a/src/commands/Project.ts +++ b/src/commands/Project.ts @@ -55,7 +55,7 @@ categories: [] SettingsListener.setFramework(framework.name); } - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } catch (err: any) { Logger.error(`Project::init: ${err?.message || err}`); Notifications.error(`Sorry, something went wrong - ${err?.message || err}`); diff --git a/src/extension.ts b/src/extension.ts index c463e958..f949b35c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,7 +15,7 @@ import { TagType } from './panelWebView/TagType'; import { ExplorerView } from './explorerView/ExplorerView'; import { Extension } from './helpers/Extension'; import { DashboardData } from './models/DashboardData'; -import { debounceCallback, Logger, Settings as SettingsHelper } from './helpers'; +import { DashboardSettings, debounceCallback, Logger, Settings as SettingsHelper } from './helpers'; import { Content } from './commands/Content'; import ContentProvider from './providers/ContentProvider'; import { Wysiwyg } from './commands/Wysiwyg'; @@ -25,6 +25,7 @@ import { Backers } from './commands/Backers'; import { DataListener, SettingsListener } from './listeners/panel'; import { NavigationType } from './dashboardWebView/models'; import { ModeSwitch } from './services/ModeSwitch'; +import { PagesParser } from './services/PagesParser'; let frontMatterStatusBar: vscode.StatusBarItem; let statusDebouncer: { (fnc: any, time: number): void; }; @@ -266,6 +267,10 @@ export async function activate(context: vscode.ExtensionContext) { // Git GitListener.init(); + // Once everything is registered, the page parsing can start in the background + DashboardSettings.get(); + PagesParser.start(); + // Subscribe all commands subscriptions.push( insertTags, diff --git a/src/helpers/DashboardSettings.ts b/src/helpers/DashboardSettings.ts index c40cc197..cd254173 100644 --- a/src/helpers/DashboardSettings.ts +++ b/src/helpers/DashboardSettings.ts @@ -16,15 +16,24 @@ import { parseWinPath } from './parseWinPath'; export class DashboardSettings { + private static cachedSettings: ISettings | undefined = undefined; - public static async get() { + public static async get(clear: boolean = false) { + if (!this.cachedSettings || clear) { + this.cachedSettings = await this.getSettings(); + } + + return this.cachedSettings; + } + + public static async getSettings() { const ext = Extension.getInstance(); const wsFolder = Folders.getWorkspaceFolder(); const isInitialized = Project.isInitialized(); const gitActions = Settings.get(SETTING_GIT_ENABLED); const pagination = Settings.get(SETTING_DASHBOARD_CONTENT_PAGINATION) - return { + const settings = { git: { isGitRepo: gitActions ? await GitListener.isGitRepository() : false, actions: gitActions || false @@ -71,7 +80,9 @@ export class DashboardSettings { dataTypes: Settings.get(SETTING_DATA_TYPES), snippets: Settings.get(SETTING_CONTENT_SNIPPETS), isBacker: await ext.getState(CONTEXT.backer, 'global') - } as ISettings + } as ISettings; + + return settings; } /** diff --git a/src/listeners/dashboard/PagesListener.ts b/src/listeners/dashboard/PagesListener.ts index af42d614..a7b92fe5 100644 --- a/src/listeners/dashboard/PagesListener.ts +++ b/src/listeners/dashboard/PagesListener.ts @@ -1,21 +1,17 @@ -import { DEFAULT_CONTENT_TYPE_NAME } from './../../constants/ContentType'; -import { isValidFile } from '../../helpers/isValidFile'; -import { existsSync, unlinkSync } from "fs"; -import { basename, dirname, join } from "path"; +import { unlinkSync } from "fs"; +import { basename } from "path"; import { commands, FileSystemWatcher, RelativePattern, TextDocument, Uri, workspace } from "vscode"; import { Dashboard } from "../../commands/Dashboard"; import { Folders } from "../../commands/Folders"; -import { COMMAND_NAME, DefaultFields, ExtensionState, SETTING_SEO_DESCRIPTION_FIELD } from "../../constants"; +import { COMMAND_NAME, ExtensionState } from "../../constants"; import { DashboardCommand } from "../../dashboardWebView/DashboardCommand"; import { DashboardMessage } from "../../dashboardWebView/DashboardMessage"; import { Page } from "../../dashboardWebView/models"; -import { ArticleHelper, Extension, Logger, Settings } from "../../helpers"; -import { ContentType } from "../../helpers/ContentType"; -import { DateHelper } from "../../helpers/DateHelper"; -import { Notifications } from "../../helpers/Notifications"; +import { ArticleHelper, Extension, Logger } from "../../helpers"; import { BaseListener } from "./BaseListener"; import { DataListener } from '../panel'; import Fuse from 'fuse.js'; +import { PagesParser } from '../../services/PagesParser'; export class PagesListener extends BaseListener { @@ -132,7 +128,7 @@ export class PagesListener extends BaseListener { if (pageIdx !== -1) { const stats = await workspace.fs.stat(file); const crntPage = this.lastPages[pageIdx]; - const updatedPage = this.processPageContent(file.fsPath, stats.mtime, basename(file.fsPath), crntPage.fmFolder); + const updatedPage = PagesParser.processPageContent(file.fsPath, stats.mtime, basename(file.fsPath), crntPage.fmFolder); if (updatedPage) { this.lastPages[pageIdx] = updatedPage; this.sendPageData(this.lastPages); @@ -156,43 +152,19 @@ export class PagesListener extends BaseListener { if (cachedPages) { this.sendPageData(cachedPages); } + } else { + PagesParser.reset(); } - // Update the dashboard with the fresh data - const folderInfo = await Folders.getInfo(); - const pages: Page[] = []; + PagesParser.getPages(async (pages: Page[]) => { + this.lastPages = pages; + this.sendPageData(pages); - if (folderInfo) { - for (const folder of folderInfo) { - for (const file of folder.lastModified) { - if (isValidFile(file.fileName)) { - try { - const page = this.processPageContent(file.filePath, file.mtime, file.fileName, folder.title); + this.sendMsg(DashboardCommand.searchReady, true); - if (page && !pages.find(p => p.fmFilePath === page.fmFilePath)) { - pages.push(page); - } - - } catch (error: any) { - if ((error as Error)?.message.toLowerCase() === "webview is disposed") { - continue; - } - - Logger.error(`PagesListener::getPagesData: ${file.filePath} - ${error.message}`); - Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`); - } - } - } - } - } - - this.lastPages = pages; - this.sendPageData(pages); - - this.sendMsg(DashboardCommand.searchReady, true); - - await ext.setState(ExtensionState.Dashboard.Pages.Cache, pages, "workspace"); - await this.createSearchIndex(pages); + await ext.setState(ExtensionState.Dashboard.Pages.Cache, pages, "workspace"); + await this.createSearchIndex(pages); + }); } /** @@ -245,136 +217,4 @@ export class PagesListener extends BaseListener { public static refresh() { this.getPagesData(true); } - - /** - * Process the page content - * @param filePath - * @param fileMtime - * @param fileName - * @param folderTitle - * @returns - */ - private static processPageContent(filePath: string, fileMtime: number, fileName: string, folderTitle: string): Page | undefined { - const article = ArticleHelper.getFrontMatterByPath(filePath); - - if (article?.data.title) { - const wsFolder = Folders.getWorkspaceFolder(); - const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description; - - const dateField = ArticleHelper.getPublishDateField(article) || DefaultFields.PublishingDate; - const dateFieldValue = article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField]) : undefined; - - const modifiedField = ArticleHelper.getModifiedDateField(article) || null; - const modifiedFieldValue = modifiedField && article?.data[modifiedField] ? DateHelper.tryParse(article?.data[modifiedField])?.getTime() : undefined; - - const staticFolder = Folders.getStaticFolderRelativePath(); - - const page: Page = { - ...article.data, - // FrontMatter properties - fmFolder: folderTitle, - fmFilePath: filePath, - fmFileName: fileName, - fmDraft: ContentType.getDraftStatus(article?.data), - fmModified: modifiedFieldValue ? modifiedFieldValue : fileMtime, - fmPublished: dateFieldValue ? dateFieldValue.getTime() : null, - fmYear: dateFieldValue ? dateFieldValue.getFullYear() : null, - fmPreviewImage: "", - fmTags: [], - fmCategories: [], - fmContentType: DEFAULT_CONTENT_TYPE_NAME, - fmBody: article?.content || "", - // Make sure these are always set - title: article?.data.title, - slug: article?.data.slug, - date: article?.data[dateField] || "", - draft: article?.data.draft, - description: article?.data[descriptionField] || "", - }; - - const contentType = ArticleHelper.getContentType(article.data); - if (contentType) { - page.fmContentType = contentType.name; - } - - let previewFieldParents = ContentType.findPreviewField(contentType.fields); - if (previewFieldParents.length === 0) { - const previewField = contentType.fields.find(field => field.type === "image" && field.name === "preview"); - if (previewField) { - previewFieldParents = ["preview"]; - } - } - - let tagParents = ContentType.findFieldByType(contentType.fields, "tags"); - const tagsValue = ContentType.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]); - page.fmTags = typeof tagsValue === "string" ? tagsValue.split(",") : tagsValue; - - let categoryParents = ContentType.findFieldByType(contentType.fields, "categories"); - const categoriesValue = ContentType.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]); - page.fmCategories = typeof categoriesValue === "string" ? categoriesValue.split(",") : categoriesValue; - - // Check if parent fields were retrieved, if not there was no image present - if (previewFieldParents.length > 0) { - let fieldValue = null; - let crntPageData = article?.data; - - for (let i = 0; i < previewFieldParents.length; i++) { - const previewField = previewFieldParents[i]; - - if (i === previewFieldParents.length - 1) { - fieldValue = crntPageData[previewField]; - } else { - if (!crntPageData[previewField]) { - continue; - } - - crntPageData = crntPageData[previewField]; - - // Check for preview image in block data - if (crntPageData instanceof Array && crntPageData.length > 0) { - // Get the first field block that contains the next field data - const fieldData = crntPageData.find(item => item[previewFieldParents[i + 1]]); - if (fieldData) { - crntPageData = fieldData; - } else { - continue; - } - } - } - } - - if (fieldValue && wsFolder) { - if (fieldValue && Array.isArray(fieldValue)) { - if (fieldValue.length > 0) { - fieldValue = fieldValue[0]; - } else { - fieldValue = undefined; - } - } - - // Revalidate as the array could have been empty - if (fieldValue) { - const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue); - const contentFolderPath = join(dirname(filePath), fieldValue); - - let previewUri = null; - if (existsSync(staticPath)) { - previewUri = Uri.file(staticPath); - } else if (existsSync(contentFolderPath)) { - previewUri = Uri.file(contentFolderPath); - } - - if (previewUri) { - const preview = Dashboard.getWebview()?.asWebviewUri(previewUri); - page["fmPreviewImage"] = preview?.toString() || ""; - } - } - } - } - - return page; - } - - return; - } } \ No newline at end of file diff --git a/src/listeners/dashboard/SettingsListener.ts b/src/listeners/dashboard/SettingsListener.ts index 728c6356..e9c7ee9b 100644 --- a/src/listeners/dashboard/SettingsListener.ts +++ b/src/listeners/dashboard/SettingsListener.ts @@ -42,15 +42,15 @@ export class SettingsListener extends BaseListener { private static async update(data: { name: string, value: any }) { if (data.name) { await Settings.update(data.name, data.value); - this.getSettings(); + this.getSettings(true); } } /** * Retrieve the settings for the dashboard */ - public static async getSettings() { - const settings = await DashboardSettings.get(); + public static async getSettings(clear: boolean = false) { + const settings = await DashboardSettings.get(clear); this.sendMsg(DashboardCommand.settings, settings); } @@ -74,7 +74,7 @@ export class SettingsListener extends BaseListener { } } - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } private static addFolder(folder: string) { diff --git a/src/listeners/dashboard/SnippetListener.ts b/src/listeners/dashboard/SnippetListener.ts index d1887c3b..f472f4cc 100644 --- a/src/listeners/dashboard/SnippetListener.ts +++ b/src/listeners/dashboard/SnippetListener.ts @@ -57,7 +57,7 @@ export class SnippetListener extends BaseListener { snippets[title] = snippetContent; await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true); - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } private static async updateSnippet(data: any) { @@ -69,7 +69,7 @@ export class SnippetListener extends BaseListener { } await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true); - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } private static async insertSnippet(data: any) { diff --git a/src/services/PagesParser.ts b/src/services/PagesParser.ts new file mode 100644 index 00000000..1bf31b8b --- /dev/null +++ b/src/services/PagesParser.ts @@ -0,0 +1,219 @@ +import { parseWinPath } from './../helpers/parseWinPath'; +import { existsSync } from "fs"; +import { dirname, join } from "path"; +import { StatusBarAlignment, Uri, window } from "vscode"; +import { Dashboard } from "../commands/Dashboard"; +import { Folders } from "../commands/Folders"; +import { DefaultFields, DEFAULT_CONTENT_TYPE_NAME, SETTING_SEO_DESCRIPTION_FIELD } from "../constants"; +import { Page } from "../dashboardWebView/models"; +import { ArticleHelper, ContentType, DateHelper, isValidFile, Logger, Notifications, Settings } from "../helpers"; + + +export class PagesParser { + public static allPages: Page[] = []; + private static parser: Promise | undefined; + private static initialized: boolean = false; + + public static start() { + if (!this.parser) { + this.parser = this.parsePages(); + } + } + + public static getPages(cb: (pages: Page[]) => void) { + if (this.parser) { + this.parser.then(() => cb(PagesParser.allPages)); + } else if (!PagesParser.initialized) { + this.parser = this.parsePages(); + this.parser.then(() => cb(PagesParser.allPages)); + } else if (PagesParser.allPages === undefined || PagesParser.allPages.length === 0) { + this.parser = this.parsePages(); + this.parser.then(() => cb(PagesParser.allPages)); + } else { + cb(PagesParser.allPages); + } + } + + public static async reset() { + this.parser = undefined; + PagesParser.allPages = []; + } + + public static async parsePages() { + // Update the dashboard with the fresh data + const folderInfo = await Folders.getInfo(); + const pages: Page[] = []; + const statusBar = window.createStatusBarItem(StatusBarAlignment.Left); + + if (folderInfo) { + statusBar.text = '$(sync~spin) Processing pages...'; + statusBar.show(); + + for (const folder of folderInfo) { + for (const file of folder.lastModified) { + if (isValidFile(file.fileName)) { + try { + const page = this.processPageContent(file.filePath, file.mtime, file.fileName, folder.title); + + if (page && !pages.find(p => p.fmFilePath === page.fmFilePath)) { + pages.push(page); + } + + } catch (error: any) { + if ((error as Error)?.message.toLowerCase() === "webview is disposed") { + continue; + } + + Logger.error(`PagesParser::parsePages: ${file.filePath} - ${error.message}`); + Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`); + } + } + } + } + } + + this.parser = undefined; + this.initialized = true; + PagesParser.allPages = [...pages]; + statusBar.hide(); + } + + /** + * Process the page content + * @param filePath + * @param fileMtime + * @param fileName + * @param folderTitle + * @returns + */ + public static processPageContent(filePath: string, fileMtime: number, fileName: string, folderTitle: string): Page | undefined { + const article = ArticleHelper.getFrontMatterByPath(filePath); + + if (article?.data.title) { + const wsFolder = Folders.getWorkspaceFolder(); + const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description; + + const dateField = ArticleHelper.getPublishDateField(article) || DefaultFields.PublishingDate; + const dateFieldValue = article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField]) : undefined; + + const modifiedField = ArticleHelper.getModifiedDateField(article) || null; + const modifiedFieldValue = modifiedField && article?.data[modifiedField] ? DateHelper.tryParse(article?.data[modifiedField])?.getTime() : undefined; + + const staticFolder = Folders.getStaticFolderRelativePath(); + + const page: Page = { + ...article.data, + // FrontMatter properties + fmFolder: folderTitle, + fmFilePath: filePath, + fmFileName: fileName, + fmDraft: ContentType.getDraftStatus(article?.data), + fmModified: modifiedFieldValue ? modifiedFieldValue : fileMtime, + fmPublished: dateFieldValue ? dateFieldValue.getTime() : null, + fmYear: dateFieldValue ? dateFieldValue.getFullYear() : null, + fmPreviewImage: "", + fmTags: [], + fmCategories: [], + fmContentType: DEFAULT_CONTENT_TYPE_NAME, + fmBody: article?.content || "", + // Make sure these are always set + title: article?.data.title, + slug: article?.data.slug, + date: article?.data[dateField] || "", + draft: article?.data.draft, + description: article?.data[descriptionField] || "", + }; + + const contentType = ArticleHelper.getContentType(article.data); + if (contentType) { + page.fmContentType = contentType.name; + } + + let previewFieldParents = ContentType.findPreviewField(contentType.fields); + if (previewFieldParents.length === 0) { + const previewField = contentType.fields.find(field => field.type === "image" && field.name === "preview"); + if (previewField) { + previewFieldParents = ["preview"]; + } + } + + let tagParents = ContentType.findFieldByType(contentType.fields, "tags"); + const tagsValue = ContentType.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]); + page.fmTags = typeof tagsValue === "string" ? tagsValue.split(",") : tagsValue; + + let categoryParents = ContentType.findFieldByType(contentType.fields, "categories"); + const categoriesValue = ContentType.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]); + page.fmCategories = typeof categoriesValue === "string" ? categoriesValue.split(",") : categoriesValue; + + // Check if parent fields were retrieved, if not there was no image present + if (previewFieldParents.length > 0) { + let fieldValue = null; + let crntPageData = article?.data; + + for (let i = 0; i < previewFieldParents.length; i++) { + const previewField = previewFieldParents[i]; + + if (i === previewFieldParents.length - 1) { + fieldValue = crntPageData[previewField]; + } else { + if (!crntPageData[previewField]) { + continue; + } + + crntPageData = crntPageData[previewField]; + + // Check for preview image in block data + if (crntPageData instanceof Array && crntPageData.length > 0) { + // Get the first field block that contains the next field data + const fieldData = crntPageData.find(item => item[previewFieldParents[i + 1]]); + if (fieldData) { + crntPageData = fieldData; + } else { + continue; + } + } + } + } + + if (fieldValue && wsFolder) { + if (fieldValue && Array.isArray(fieldValue)) { + if (fieldValue.length > 0) { + fieldValue = fieldValue[0]; + } else { + fieldValue = undefined; + } + } + + // Revalidate as the array could have been empty + if (fieldValue) { + const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue); + const contentFolderPath = join(dirname(filePath), fieldValue); + + let previewUri = null; + if (existsSync(staticPath)) { + previewUri = Uri.file(staticPath); + } else if (existsSync(contentFolderPath)) { + previewUri = Uri.file(contentFolderPath); + } + + if (previewUri) { + const previewPath = Dashboard.getWebview()?.asWebviewUri(previewUri); + let preview = previewPath?.toString(); + + if (!preview) { + const fileUrl = parseWinPath(previewUri.fsPath); + preview = `https://file%2B.vscode-resource.vscode-cdn.net/${fileUrl.startsWith(`/`) ? fileUrl.substr(1) : fileUrl}`; + } + + page["fmPreviewImage"] = preview?.toString() || ""; + } + } + } + } + + return page; + } + + return; + } +} \ No newline at end of file From 726a26850d0f5912032d381dbef3196720bda14a Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Sun, 2 Oct 2022 14:23:51 +0200 Subject: [PATCH 2/6] #431 - Cache changes + Tab navigation --- src/dashboardWebView/hooks/usePages.tsx | 63 +++++++++++++++++------- src/dashboardWebView/models/Page.ts | 6 ++- src/listeners/dashboard/PagesListener.ts | 3 +- src/services/PagesParser.ts | 50 +++++++++++++++++-- 4 files changed, 95 insertions(+), 27 deletions(-) diff --git a/src/dashboardWebView/hooks/usePages.tsx b/src/dashboardWebView/hooks/usePages.tsx index f54a685d..d290aa5a 100644 --- a/src/dashboardWebView/hooks/usePages.tsx +++ b/src/dashboardWebView/hooks/usePages.tsx @@ -13,6 +13,7 @@ import { parseWinPath } from '../../helpers/parseWinPath'; export default function usePages(pages: Page[]) { const [ pageItems, setPageItems ] = useState([]); + const [ sortedPages, setSortedPages ] = useState([]); const [ sorting, setSorting ] = useRecoilState(SortingAtom); const [ tabInfo , setTabInfo ] = useRecoilState(TabInfoAtom); const settings = useRecoilValue(SettingsSelector); @@ -22,8 +23,10 @@ export default function usePages(pages: Page[]) { const tag = useRecoilValue(TagSelector); const category = useRecoilValue(CategorySelector); - const processPages = useCallback((searchedPages: Page[]) => { - const draftField = settings?.draftField; + /** + * Process all the pages by applying the sorting, filtering and searching. + */ + const processPages = useCallback((searchedPages: Page[], fullProcess: boolean = true) => { const framework = settings?.crntFramework; // Filter the pages @@ -93,40 +96,52 @@ export default function usePages(pages: Page[]) { pagesSorted = pagesSorted.filter(page => page.fmCategories && page.fmCategories.includes(category)); } + setSortedPages(pagesSorted); + }, [ settings, tab, folder, search, tag, category, sorting, tabInfo ]); + + + /** + * Process the pages when the tab changes + */ + const processByTab = useCallback((pages: Page[]) => { + const draftField = settings?.draftField; + + let crntPages: Page[] = Object.assign([], pages); + // Process the tab data const draftTypes = Object.assign({}, tabInfo); - draftTypes[Tab.All] = pagesSorted.length; + draftTypes[Tab.All] = crntPages.length; // Filter by draft status if (draftField && draftField.type === 'choice') { const draftChoices = settings?.draftField?.choices; for (const choice of (draftChoices || [])) { if (choice) { - draftTypes[choice] = pagesSorted.filter(page => page.fmDraft === choice).length; + draftTypes[choice] = crntPages.filter(page => page.fmDraft === choice).length; } } if (tab !== Tab.All) { - pagesSorted = pagesSorted.filter(page => page.fmDraft === tab); + crntPages = crntPages.filter(page => page.fmDraft === tab); } else { - pagesSorted = pagesSorted; + crntPages = crntPages; } } else { // Draft field is a boolean field const draftFieldName = draftField?.name || "draft"; - const drafts = pagesSorted.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true"); - const published = pagesSorted.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined"); + const drafts = crntPages.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true"); + const published = crntPages.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined"); draftTypes[Tab.Draft] = draftField?.invert ? published.length : drafts.length; draftTypes[Tab.Published] = draftField?.invert ? drafts.length : published.length; if (tab === Tab.Published) { - pagesSorted = draftField?.invert ? drafts : published; + crntPages = draftField?.invert ? drafts : published; } else if (tab === Tab.Draft) { - pagesSorted = draftField?.invert ? published : drafts; + crntPages = draftField?.invert ? published : drafts; } else { - pagesSorted = pagesSorted; + crntPages = crntPages; } } @@ -134,10 +149,14 @@ export default function usePages(pages: Page[]) { setTabInfo(draftTypes); // Set the pages - setPageItems(pagesSorted); - }, [ settings, tab, folder, search, tag, category, sorting, tabInfo ]); - + setPageItems(crntPages); + }, [ tab, tabInfo, settings ]); + + /** + * Search listener for filtered pages + * @param message + */ const searchListener = (message: MessageEvent>) => { switch (message.data.command) { case DashboardMessage.searchPages: @@ -146,6 +165,7 @@ export default function usePages(pages: Page[]) { } }; + useEffect(() => { let usedSorting = sorting; @@ -160,15 +180,20 @@ export default function usePages(pages: Page[]) { // Check if search needs to be performed let searchedPages = pages; if (search) { - // const fuse = new Fuse(pages, fuseOptions); - // const results = fuse.search(search); - // searchedPages = results.map(page => page.item); - Messenger.send(DashboardMessage.searchPages, { query: search }); } else { processPages(searchedPages); } - }, [ settings?.draftField, pages, sorting, search, tab, tag, category, folder ]); + }, [ settings?.draftField, pages, sorting, search, tag, category, folder ]); + + + useEffect(() => { + console.log("useEffect: tab", tab, sortedPages.length); + if (sortedPages.length > 0) { + processByTab(sortedPages); + } + }, [sortedPages, tab]) + useEffect(() => { Messenger.listen(searchListener); diff --git a/src/dashboardWebView/models/Page.ts b/src/dashboardWebView/models/Page.ts index 3d4dc1d7..ce690799 100644 --- a/src/dashboardWebView/models/Page.ts +++ b/src/dashboardWebView/models/Page.ts @@ -1,6 +1,10 @@ -import { Uri } from "vscode"; export interface Page { + // Properties for caching + fmCachePath: string; + fmCacheModifiedTime: number; + + // Front matter fields fmFolder: string; fmFilePath: string; fmFileName: string; diff --git a/src/listeners/dashboard/PagesListener.ts b/src/listeners/dashboard/PagesListener.ts index a7b92fe5..37531d6b 100644 --- a/src/listeners/dashboard/PagesListener.ts +++ b/src/listeners/dashboard/PagesListener.ts @@ -161,8 +161,7 @@ export class PagesListener extends BaseListener { this.sendPageData(pages); this.sendMsg(DashboardCommand.searchReady, true); - - await ext.setState(ExtensionState.Dashboard.Pages.Cache, pages, "workspace"); + await this.createSearchIndex(pages); }); } diff --git a/src/services/PagesParser.ts b/src/services/PagesParser.ts index 1bf31b8b..4a7bb6c6 100644 --- a/src/services/PagesParser.ts +++ b/src/services/PagesParser.ts @@ -4,22 +4,30 @@ import { dirname, join } from "path"; import { StatusBarAlignment, Uri, window } from "vscode"; import { Dashboard } from "../commands/Dashboard"; import { Folders } from "../commands/Folders"; -import { DefaultFields, DEFAULT_CONTENT_TYPE_NAME, SETTING_SEO_DESCRIPTION_FIELD } from "../constants"; +import { DefaultFields, DEFAULT_CONTENT_TYPE_NAME, ExtensionState, SETTING_SEO_DESCRIPTION_FIELD } from "../constants"; import { Page } from "../dashboardWebView/models"; -import { ArticleHelper, ContentType, DateHelper, isValidFile, Logger, Notifications, Settings } from "../helpers"; +import { ArticleHelper, ContentType, DateHelper, Extension, isValidFile, Logger, Notifications, Settings } from "../helpers"; export class PagesParser { public static allPages: Page[] = []; + public static cachedPages: Page[] | undefined = undefined; private static parser: Promise | undefined; private static initialized: boolean = false; + /** + * Start the page parser + */ public static start() { if (!this.parser) { this.parser = this.parsePages(); } } + /** + * Retrieve the pages + * @param cb + */ public static getPages(cb: (pages: Page[]) => void) { if (this.parser) { this.parser.then(() => cb(PagesParser.allPages)); @@ -34,12 +42,20 @@ export class PagesParser { } } + /** + * Reset the cache + */ public static async reset() { this.parser = undefined; PagesParser.allPages = []; } + /** + * Parse all pages in the workspace + */ public static async parsePages() { + const ext = Extension.getInstance(); + // Update the dashboard with the fresh data const folderInfo = await Folders.getInfo(); const pages: Page[] = []; @@ -53,12 +69,15 @@ export class PagesParser { for (const file of folder.lastModified) { if (isValidFile(file.fileName)) { try { - const page = this.processPageContent(file.filePath, file.mtime, file.fileName, folder.title); + let page = await PagesParser.getCachedPage(file.filePath, file.mtime); - if (page && !pages.find(p => p.fmFilePath === page.fmFilePath)) { + if (!page) { + page = this.processPageContent(file.filePath, file.mtime, file.fileName, folder.title); + } + + if (page && !pages.find(p => p.fmFilePath === page?.fmFilePath)) { pages.push(page); } - } catch (error: any) { if ((error as Error)?.message.toLowerCase() === "webview is disposed") { continue; @@ -72,12 +91,30 @@ export class PagesParser { } } + await ext.setState(ExtensionState.Dashboard.Pages.Cache, pages, "workspace"); + PagesParser.cachedPages = undefined; + this.parser = undefined; this.initialized = true; PagesParser.allPages = [...pages]; statusBar.hide(); } + /** + * Find the page in the cached data + * @param filePath + * @param modifiedTime + * @returns + */ + public static async getCachedPage(filePath: string, modifiedTime: number): Promise { + if (!PagesParser.cachedPages) { + const ext = Extension.getInstance(); + PagesParser.cachedPages = await ext.getState(ExtensionState.Dashboard.Pages.Cache, "workspace") || []; + } + + return PagesParser.cachedPages.find(p => p.fmCachePath === parseWinPath(filePath) && p.fmCacheModifiedTime === modifiedTime); + } + /** * Process the page content * @param filePath @@ -103,6 +140,9 @@ export class PagesParser { const page: Page = { ...article.data, + // Cache properties + fmCachePath: parseWinPath(filePath), + fmCacheModifiedTime: fileMtime, // FrontMatter properties fmFolder: folderTitle, fmFilePath: filePath, From 0c6ae47a7b6ceef9940c42d134160da18fa850d4 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Sun, 2 Oct 2022 14:25:11 +0200 Subject: [PATCH 3/6] #434 - Webview errors are logged in the extension output --- CHANGELOG.md | 1 + src/commands/Dashboard.ts | 3 +- src/dashboardWebView/DashboardMessage.ts | 1 + src/dashboardWebView/components/App.tsx | 44 ++++++++++++------- .../components/ErrorView/index.tsx | 14 ++++++ src/listeners/dashboard/LogListener.ts | 21 +++++++++ src/listeners/dashboard/index.ts | 1 + 7 files changed, 68 insertions(+), 17 deletions(-) create mode 100644 src/dashboardWebView/components/ErrorView/index.tsx create mode 100644 src/listeners/dashboard/LogListener.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bd0190d9..ba79bc65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - [#406](https://github.com/estruyf/vscode-front-matter/issues/406): Added support for single data entries in the data dashboard - [#428](https://github.com/estruyf/vscode-front-matter/issues/428): Improved UX for inserting images to your content +- [#434](https://github.com/estruyf/vscode-front-matter/issues/434): Webview errors are logged in the extension output ### ⚡️ Optimizations diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index d34e682f..2bcc13e4 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -7,7 +7,7 @@ import { Extension } from '../helpers/Extension'; import { WebviewHelper } from '@estruyf/vscode'; import { DashboardData } from '../models/DashboardData'; import { MediaLibrary } from '../helpers/MediaLibrary'; -import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener, SnippetListener, TaxonomyListener } from '../listeners/dashboard'; +import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener, SnippetListener, TaxonomyListener, LogListener } from '../listeners/dashboard'; import { MediaListener as PanelMediaListener } from '../listeners/panel' import { GitListener, ModeListener } from '../listeners/general'; @@ -148,6 +148,7 @@ export class Dashboard { ModeListener.process(msg); GitListener.process(msg); TaxonomyListener.process(msg); + LogListener.process(msg); }); } diff --git a/src/dashboardWebView/DashboardMessage.ts b/src/dashboardWebView/DashboardMessage.ts index a261c287..a09ae77d 100644 --- a/src/dashboardWebView/DashboardMessage.ts +++ b/src/dashboardWebView/DashboardMessage.ts @@ -57,4 +57,5 @@ export enum DashboardMessage { setState = 'setState', runCustomScript = 'runCustomScript', sendTelemetry = 'sendTelemetry', + logError = 'logError', } \ No newline at end of file diff --git a/src/dashboardWebView/components/App.tsx b/src/dashboardWebView/components/App.tsx index 36085faf..9c56f034 100644 --- a/src/dashboardWebView/components/App.tsx +++ b/src/dashboardWebView/components/App.tsx @@ -16,6 +16,9 @@ import { Route, Routes, useNavigate } from 'react-router-dom'; import { routePaths } from '..'; import { useEffect, useMemo } from 'react'; import { UnknownView } from './UnknownView'; +import { ErrorBoundary } from '@sentry/react'; +import { ErrorView } from './ErrorView'; +import { DashboardMessage } from '../DashboardMessage'; export interface IAppProps { showWelcome: boolean; @@ -68,23 +71,32 @@ export const App: React.FunctionComponent = ({showWelcome}: React.Pro } return ( -
- - } /> - } /> - } /> - } /> - - { - allowDataView && } /> - } + )} + onError={(error: Error, componentStack: string, eventId: string) => { + Messenger.send(DashboardMessage.logError, `Event ID: ${eventId} +Message: ${error.message} - { - allowTaxonomyView && } /> - } +Stack: ${componentStack}`); + }}> +
+ + } /> + } /> + } /> + } /> + + { + allowDataView && } /> + } - } /> - -
+ { + allowTaxonomyView && } /> + } + + } /> +
+
+ ); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/ErrorView/index.tsx b/src/dashboardWebView/components/ErrorView/index.tsx new file mode 100644 index 00000000..17efb741 --- /dev/null +++ b/src/dashboardWebView/components/ErrorView/index.tsx @@ -0,0 +1,14 @@ +import { ExclamationIcon } from '@heroicons/react/solid'; +import * as React from 'react'; + +export interface IErrorViewProps {} + +export const ErrorView: React.FunctionComponent = (props: React.PropsWithChildren) => { + return ( +
+ +

Sorry, something went wrong.

+

Please close the dashboard and try again.

+
+ ); +}; \ No newline at end of file diff --git a/src/listeners/dashboard/LogListener.ts b/src/listeners/dashboard/LogListener.ts new file mode 100644 index 00000000..2c43f12c --- /dev/null +++ b/src/listeners/dashboard/LogListener.ts @@ -0,0 +1,21 @@ +import { DashboardMessage } from "../../dashboardWebView/DashboardMessage"; +import { Logger } from "../../helpers"; +import { BaseListener } from "./BaseListener"; + + +export class LogListener extends BaseListener { + + /** + * Process the messages for the dashboard views + * @param msg + */ + public static process(msg: { command: DashboardMessage, data: any }) { + super.process(msg); + + switch(msg.command) { + case DashboardMessage.logError: + Logger.error(msg.data); + break; + } + } +} \ No newline at end of file diff --git a/src/listeners/dashboard/index.ts b/src/listeners/dashboard/index.ts index a26e99d8..8ede40ed 100644 --- a/src/listeners/dashboard/index.ts +++ b/src/listeners/dashboard/index.ts @@ -8,3 +8,4 @@ export * from './SettingsListener'; export * from './SnippetListener'; export * from './TelemetryListener'; export * from './TaxonomyListener'; +export * from './LogListener'; From 78002563be0ae5b813b84f185bece1ed43752865 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 3 Oct 2022 13:22:40 +0200 Subject: [PATCH 4/6] Clear cache command --- package.json | 5 ++++ src/commands/Cache.ts | 24 +++++++++++++++++++ src/commands/index.ts | 10 ++++++++ src/constants/Extension.ts | 3 +++ .../components/Contents/Item.tsx | 4 ++-- src/extension.ts | 4 ++++ src/services/PagesParser.ts | 14 +++++++++-- 7 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 src/commands/Cache.ts diff --git a/package.json b/package.json index 70f986d1..d67f9d0d 100644 --- a/package.json +++ b/package.json @@ -1742,6 +1742,11 @@ "command": "frontMatter.git.sync", "title": "Sync", "category": "Front Matter" + }, + { + "command": "frontMatter.cache.clear", + "title": "Clear cache", + "category": "Front Matter" } ], "menus": { diff --git a/src/commands/Cache.ts b/src/commands/Cache.ts new file mode 100644 index 00000000..5175f5b8 --- /dev/null +++ b/src/commands/Cache.ts @@ -0,0 +1,24 @@ +import { commands } from "vscode"; +import { COMMAND_NAME, ExtensionState } from "../constants"; +import { Extension, Notifications } from "../helpers"; + +export class Cache { + + public static async registerCommands() { + const ext = Extension.getInstance(); + const subscriptions = ext.subscriptions; + + subscriptions.push( + commands.registerCommand(COMMAND_NAME.clearCache, Cache.clear) + ); + } + + private static async clear() { + const ext = Extension.getInstance(); + + await ext.setState(ExtensionState.Dashboard.Pages.Cache, undefined, "workspace"); + await ext.setState(ExtensionState.Dashboard.Pages.Index, undefined, "workspace"); + + Notifications.info("Cache cleared"); + } +} \ No newline at end of file diff --git a/src/commands/index.ts b/src/commands/index.ts index d5aa6bdc..7c391ede 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,3 +1,13 @@ export * from './Article'; +export * from './Backers'; +export * from './Cache'; +export * from './Content'; +export * from './Dashboard'; +export * from './Diagnostics'; +export * from './Folders'; +export * from './Preview'; +export * from './Project'; export * from './Settings'; export * from './StatusListener'; +export * from './Template'; +export * from './Wysiwyg'; diff --git a/src/constants/Extension.ts b/src/constants/Extension.ts index 6e0eb252..4dc68df7 100644 --- a/src/constants/Extension.ts +++ b/src/constants/Extension.ts @@ -72,4 +72,7 @@ export const COMMAND_NAME = { // Config reloadConfig: getCommandName("config.reload"), + + // Cache + clearCache: getCommandName("cache.clear"), }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Contents/Item.tsx b/src/dashboardWebView/components/Contents/Item.tsx index 3f140774..2b7f3d4a 100644 --- a/src/dashboardWebView/components/Contents/Item.tsx +++ b/src/dashboardWebView/components/Contents/Item.tsx @@ -95,9 +95,9 @@ export const Item: React.FunctionComponent = ({ fmFilePath, date, ti onOpen={openFile} /> - + - + { tags && tags.length > 0 && ( diff --git a/src/extension.ts b/src/extension.ts index f949b35c..40ec3644 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,6 +8,7 @@ import { Folders } from './commands/Folders'; import { Preview } from './commands/Preview'; import { Project } from './commands/Project'; import { Template } from './commands/Template'; +import { Cache } from './commands/Cache'; import { COMMAND_NAME, TelemetryEvent } from './constants'; import { TaxonomyType } from './models'; import { MarkdownFoldingProvider } from './providers/MarkdownFoldingProvider'; @@ -271,6 +272,9 @@ export async function activate(context: vscode.ExtensionContext) { DashboardSettings.get(); PagesParser.start(); + // Cache commands + Cache.registerCommands(); + // Subscribe all commands subscriptions.push( insertTags, diff --git a/src/services/PagesParser.ts b/src/services/PagesParser.ts index 4a7bb6c6..7c50b787 100644 --- a/src/services/PagesParser.ts +++ b/src/services/PagesParser.ts @@ -138,6 +138,16 @@ export class PagesParser { const staticFolder = Folders.getStaticFolderRelativePath(); + let escapedTitle = article?.data.title; + if (escapedTitle && typeof escapedTitle !== "string") { + escapedTitle = ""; + } + + let escapedDescription = article?.data[descriptionField] || ""; + if (escapedDescription && typeof escapedDescription !== "string") { + escapedDescription = ""; + } + const page: Page = { ...article.data, // Cache properties @@ -157,11 +167,11 @@ export class PagesParser { fmContentType: DEFAULT_CONTENT_TYPE_NAME, fmBody: article?.content || "", // Make sure these are always set - title: article?.data.title, + title: escapedTitle, slug: article?.data.slug, date: article?.data[dateField] || "", draft: article?.data.draft, - description: article?.data[descriptionField] || "", + description: escapedDescription, }; const contentType = ArticleHelper.getContentType(article.data); From 45eb542619fcdeec1f6017ac278d6823634a7078 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 3 Oct 2022 21:31:23 +0200 Subject: [PATCH 5/6] #431 - Allow pagination page nr --- package.json | 4 +- .../components/Contents/Overview.tsx | 9 +-- .../components/Header/Header.tsx | 13 ++-- .../components/Header/Pagination.tsx | 36 +++++------ .../components/Header/PaginationStatus.tsx | 29 +++++---- src/dashboardWebView/hooks/useMedia.tsx | 13 ++-- src/dashboardWebView/hooks/usePages.tsx | 1 - src/dashboardWebView/hooks/usePagination.tsx | 62 +++++++++++++++++++ src/dashboardWebView/models/Settings.ts | 2 +- src/helpers/DashboardSettings.ts | 2 +- 10 files changed, 120 insertions(+), 51 deletions(-) create mode 100644 src/dashboardWebView/hooks/usePagination.tsx diff --git a/package.json b/package.json index d67f9d0d..c32a2db2 100644 --- a/package.json +++ b/package.json @@ -447,9 +447,9 @@ "scope": "Custom scripts" }, "frontMatter.dashboard.content.pagination": { - "type": "boolean", + "type": ["boolean", "number"], "default": true, - "markdownDescription": "Specify if you want to enable/disable pagination for your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.content.pagination)", + "markdownDescription": "Specify if you want to enable/disable pagination for your content. You can define your page number up to 52. Default items per page is `16`. Disabling the pagination can be done by setting it to `false`. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.content.pagination)", "scope": "Dashboard" }, "frontMatter.dashboard.content.cardTags": { diff --git a/src/dashboardWebView/components/Contents/Overview.tsx b/src/dashboardWebView/components/Contents/Overview.tsx index b83f1c7b..3cd2a760 100644 --- a/src/dashboardWebView/components/Contents/Overview.tsx +++ b/src/dashboardWebView/components/Contents/Overview.tsx @@ -9,9 +9,9 @@ import { GroupOption } from '../../constants/GroupOption'; import { Page } from '../../models/Page'; import { Settings } from '../../models/Settings'; import { GroupingSelector, PageAtom } from '../../state'; -import { PAGE_LIMIT } from '../Header/Pagination'; import { Item } from './Item'; import { List } from './List'; +import usePagination from '../../hooks/usePagination'; export interface IOverviewProps { pages: Page[]; @@ -21,14 +21,15 @@ export interface IOverviewProps { export const Overview: React.FunctionComponent = ({pages, settings}: React.PropsWithChildren) => { const grouping = useRecoilValue(GroupingSelector); const page = useRecoilValue(PageAtom); + const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination); const pagedPages = useMemo(() => { - if (settings?.dashboardState.contents.pagination) { - return pages.slice(page * PAGE_LIMIT, ((page + 1) * PAGE_LIMIT)); + if (pageSetNr) { + return pages.slice(page * pageSetNr, ((page + 1) * pageSetNr)); } return pages; - }, [pages, page, settings]); + }, [pages, page, pageSetNr]); const groupName = useCallback((groupId, groupedPages) => { if (grouping === GroupOption.Draft) { diff --git a/src/dashboardWebView/components/Header/Header.tsx b/src/dashboardWebView/components/Header/Header.tsx index 5be485bb..230ddeeb 100644 --- a/src/dashboardWebView/components/Header/Header.tsx +++ b/src/dashboardWebView/components/Header/Header.tsx @@ -23,8 +23,10 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { routePaths } from '../..'; import { useEffect, useMemo } from 'react'; import { SyncButton } from './SyncButton'; -import { PAGE_LIMIT, Pagination } from './Pagination'; +import { Pagination } from './Pagination'; import { GroupOption } from '../../constants/GroupOption'; +import usePagination from '../../hooks/usePagination'; +import { PaginationStatus } from './PaginationStatus'; export interface IHeaderProps { header?: React.ReactNode; @@ -37,13 +39,14 @@ export interface IHeaderProps { folders?: string[]; } -export const Header: React.FunctionComponent = ({header, totalPages, folders, settings }: React.PropsWithChildren) => { +export const Header: React.FunctionComponent = ({header, totalPages, settings }: React.PropsWithChildren) => { const [ crntTag, setCrntTag ] = useRecoilState(TagAtom); const [ crntCategory, setCrntCategory ] = useRecoilState(CategoryAtom); const grouping = useRecoilValue(GroupingSelector); const resetSorting = useResetRecoilState(SortingAtom); const location = useLocation(); const navigate = useNavigate(); + const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination); const createContent = () => { Messenger.send(DashboardMessage.createContent); @@ -180,8 +183,10 @@ export const Header: React.FunctionComponent = ({header, totalPage { - (settings?.dashboardState.contents.pagination) && (totalPages || 0) > PAGE_LIMIT && (!grouping || grouping === GroupOption.none) && ( -
+ (pageSetNr > 0) && (totalPages || 0) > pageSetNr && (!grouping || grouping === GroupOption.none) && ( +
+ +
) diff --git a/src/dashboardWebView/components/Header/Pagination.tsx b/src/dashboardWebView/components/Header/Pagination.tsx index 3b52baf3..967046a8 100644 --- a/src/dashboardWebView/components/Header/Pagination.tsx +++ b/src/dashboardWebView/components/Header/Pagination.tsx @@ -1,43 +1,37 @@ import * as React from 'react'; -import { useEffect, useMemo } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useCallback, useEffect, useMemo } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { routePaths } from '../..'; -import { MediaTotalSelector, PageAtom } from '../../state'; +import usePagination from '../../hooks/usePagination'; +import { MediaTotalSelector, PageAtom, SettingsAtom } from '../../state'; import { PaginationButton } from './PaginationButton'; export interface IPaginationProps { totalPages?: number; } -export const PAGE_LIMIT = 16; - export const Pagination: React.FunctionComponent = ({ totalPages }: React.PropsWithChildren) => { const [ page, setPage ] = useRecoilState(PageAtom); const totalMedia = useRecoilValue(MediaTotalSelector); - const location = useLocation(); + const settings = useRecoilValue(SettingsAtom); + const { pageSetNr, totalPagesNr } = usePagination(settings?.dashboardState.contents.pagination, totalPages, totalMedia); - const totalItems: number = useMemo(() => { - if (location.pathname === routePaths.contents) { - return Math.ceil((totalPages || 0) / PAGE_LIMIT) - 1 - } else { - return Math.ceil(totalMedia / PAGE_LIMIT) - 1; - } - }, [location.pathname, totalPages, totalMedia]); - - const getButtons = (): number[] => { + const getButtons = useCallback((): number[] => { const maxButtons = 5; const buttons: number[] = []; const start = page - maxButtons; const end = page + maxButtons; for (let i = start; i <= end; i++) { - if (i >= 0 && i <= totalItems) { + if (i >= 0 && i <= totalPagesNr) { buttons.push(i); } } return buttons; - }; + }, [page, totalPagesNr]); + + useEffect(() => { + setPage(0); + }, [pageSetNr]); useEffect(() => { setPage(0); @@ -77,13 +71,13 @@ export const Pagination: React.FunctionComponent = ({ totalPag = totalItems} + disabled={page >= totalPagesNr} onClick={() => setPage(page + 1)} /> = totalItems} - onClick={() => setPage(totalItems)} /> + disabled={page >= totalPagesNr} + onClick={() => setPage(totalPagesNr)} />
); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Header/PaginationStatus.tsx b/src/dashboardWebView/components/Header/PaginationStatus.tsx index b145e18e..ce3caea6 100644 --- a/src/dashboardWebView/components/Header/PaginationStatus.tsx +++ b/src/dashboardWebView/components/Header/PaginationStatus.tsx @@ -1,27 +1,32 @@ import * as React from 'react'; +import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; -import { MediaTotalSelector, PageAtom } from '../../state'; -import { PAGE_LIMIT } from './Pagination'; +import usePagination from '../../hooks/usePagination'; +import { MediaTotalSelector, PageAtom, SettingsAtom } from '../../state'; -export interface IPaginationStatusProps {} +export interface IPaginationStatusProps { + totalPages?: number; +} -export const PaginationStatus: React.FunctionComponent = (props: React.PropsWithChildren) => { +export const PaginationStatus: React.FunctionComponent = ({ totalPages }: React.PropsWithChildren) => { const totalMedia = useRecoilValue(MediaTotalSelector); const page = useRecoilValue(PageAtom); + const settings = useRecoilValue(SettingsAtom); + const { pageSetNr, totalItems } = usePagination(settings?.dashboardState.contents.pagination, totalPages || 0, totalMedia); - const getTotalPage = () => { - const mediaItems = ((page + 1) * PAGE_LIMIT); - if (totalMedia < mediaItems) { - return totalMedia; + const totelItemsOnPage = useMemo(() => { + const items = ((page + 1) * pageSetNr); + if (totalItems < items) { + return totalItems; } - return mediaItems; - }; + return totalItems; + }, [page, totalMedia, pageSetNr]); return (

- Showing {(page * PAGE_LIMIT) + 1} to {getTotalPage()} of{' '} - {totalMedia} results + Showing {(page * pageSetNr) + 1} to {totelItemsOnPage} of{' '} + {totalItems} results

); diff --git a/src/dashboardWebView/hooks/useMedia.tsx b/src/dashboardWebView/hooks/useMedia.tsx index 95fcd21c..44c4ddeb 100644 --- a/src/dashboardWebView/hooks/useMedia.tsx +++ b/src/dashboardWebView/hooks/useMedia.tsx @@ -4,9 +4,9 @@ import { useState, useEffect, useCallback } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { MediaInfo, MediaPaths } from '../../models'; import { DashboardCommand } from '../DashboardCommand'; -import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, PageAtom, SearchAtom, SelectedMediaFolderAtom } from '../state'; +import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, PageAtom, SearchAtom, SelectedMediaFolderAtom, SettingsAtom } from '../state'; import Fuse from 'fuse.js'; -import { PAGE_LIMIT } from '../components/Header/Pagination'; +import usePagination from './usePagination'; const fuseOptions: Fuse.IFuseOptions = { keys: [ @@ -28,10 +28,12 @@ export default function useMedia() { const [ , setFolders ] = useRecoilState(MediaFoldersAtom); const [ , setLoading ] = useRecoilState(LoadingAtom); const search = useRecoilValue(SearchAtom); + const settings = useRecoilValue(SettingsAtom); + const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination); const getMedia = useCallback(() => { - return searchedMedia.slice(page * PAGE_LIMIT, ((page + 1) * PAGE_LIMIT)); - }, [searchedMedia, page]); + return searchedMedia.slice(page * pageSetNr, ((page + 1) * pageSetNr)); + }, [searchedMedia, page, pageSetNr]); const messageListener = (message: MessageEvent>) => { if (message.data.command === DashboardCommand.media) { @@ -57,8 +59,9 @@ export default function useMedia() { return; } + setTotal(media.length); setSearchedMedia(media); - }, [search]); + }, [search, media]); useEffect(() => { Messenger.listen(messageListener); diff --git a/src/dashboardWebView/hooks/usePages.tsx b/src/dashboardWebView/hooks/usePages.tsx index d290aa5a..f36c496c 100644 --- a/src/dashboardWebView/hooks/usePages.tsx +++ b/src/dashboardWebView/hooks/usePages.tsx @@ -188,7 +188,6 @@ export default function usePages(pages: Page[]) { useEffect(() => { - console.log("useEffect: tab", tab, sortedPages.length); if (sortedPages.length > 0) { processByTab(sortedPages); } diff --git a/src/dashboardWebView/hooks/usePagination.tsx b/src/dashboardWebView/hooks/usePagination.tsx new file mode 100644 index 00000000..4cf5a5b1 --- /dev/null +++ b/src/dashboardWebView/hooks/usePagination.tsx @@ -0,0 +1,62 @@ +import { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; +import { routePaths } from '..'; + +export const PAGE_LIMIT = 16; + +export default function usePagination(value: number | boolean | null | undefined, totalPages?: number, totalMedia?: number) { + const location = useLocation(); + + const pagination = useMemo(() => { + if (location.pathname === routePaths.contents) { + if (typeof value === 'number') { + const pageNr = value > 0 ? value : 0; + if (pageNr > 52) { + return 52; + } + return pageNr; + } else if (typeof value === 'boolean') { + return value ? PAGE_LIMIT : 0; + } + } + + return PAGE_LIMIT; + }, [value, location.pathname]); + + + const totalPagesNr: number = useMemo(() => { + if (location.pathname === routePaths.contents) { + if (totalPages) { + return Math.ceil((totalPages || 0) / pagination) - 1 + } + } else { + if (totalMedia) { + return Math.ceil(totalMedia / pagination) - 1; + } + } + return 0; + }, [location.pathname, totalPages, totalMedia, pagination]); + + /** + * The total items (pages or media) + */ + const totalItems: number = useMemo(() => { + if (location.pathname === routePaths.contents) { + if (totalPages) { + return totalPages; + } + } else { + if (totalMedia) { + return totalMedia; + } + } + return 0; + }, [location.pathname, totalPages, totalMedia, pagination]); + + + return { + pageSetNr: pagination, + totalPagesNr, + totalItems + }; +} \ No newline at end of file diff --git a/src/dashboardWebView/models/Settings.ts b/src/dashboardWebView/models/Settings.ts index ee1ab44e..a29cc4d2 100644 --- a/src/dashboardWebView/models/Settings.ts +++ b/src/dashboardWebView/models/Settings.ts @@ -44,7 +44,7 @@ export interface ContentsViewState { defaultSorting: string | null | undefined; tags: string | null | undefined; templatesEnabled: boolean | null | undefined; - pagination: boolean | null | undefined; + pagination: boolean | number | null | undefined; } export interface MediaViewState extends ContentsViewState { diff --git a/src/helpers/DashboardSettings.ts b/src/helpers/DashboardSettings.ts index cd254173..6119a1fb 100644 --- a/src/helpers/DashboardSettings.ts +++ b/src/helpers/DashboardSettings.ts @@ -31,7 +31,7 @@ export class DashboardSettings { const wsFolder = Folders.getWorkspaceFolder(); const isInitialized = Project.isInitialized(); const gitActions = Settings.get(SETTING_GIT_ENABLED); - const pagination = Settings.get(SETTING_DASHBOARD_CONTENT_PAGINATION) + const pagination = Settings.get(SETTING_DASHBOARD_CONTENT_PAGINATION) const settings = { git: { From 888e5c5229a063776f49f4bfa5bcde2ca3cd859b Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 4 Oct 2022 13:47:41 +0200 Subject: [PATCH 6/6] Get webview URI for Windows --- src/services/PagesParser.ts | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/services/PagesParser.ts b/src/services/PagesParser.ts index 7c50b787..24bb3b8a 100644 --- a/src/services/PagesParser.ts +++ b/src/services/PagesParser.ts @@ -247,15 +247,13 @@ export class PagesParser { } if (previewUri) { - const previewPath = Dashboard.getWebview()?.asWebviewUri(previewUri); - let preview = previewPath?.toString(); + let previewPath = Dashboard.getWebview()?.asWebviewUri(previewUri); - if (!preview) { - const fileUrl = parseWinPath(previewUri.fsPath); - preview = `https://file%2B.vscode-resource.vscode-cdn.net/${fileUrl.startsWith(`/`) ? fileUrl.substr(1) : fileUrl}`; + if (!previewPath) { + previewPath = PagesParser.getWebviewUri(previewUri); } - page["fmPreviewImage"] = preview?.toString() || ""; + page["fmPreviewImage"] = previewPath?.toString() || ""; } } } @@ -266,4 +264,24 @@ export class PagesParser { return; } + + /** + * Get the webview URI + * @param resource + * @returns + */ + private static getWebviewUri(resource: Uri) { + // Logic from: https://github.com/microsoft/vscode/blob/main/src/vs/workbench/common/webview.ts + const webviewResourceBaseHost = 'vscode-cdn.net'; + const webviewRootResourceAuthority = `vscode-resource.${webviewResourceBaseHost}`; + + const authority = `${resource.scheme}+${encodeURI(resource.authority)}.${webviewRootResourceAuthority}`; + return Uri.from({ + scheme: "https", + authority, + path: resource.path, + query: resource.query, + fragment: resource.fragment + }); + } } \ No newline at end of file