diff --git a/CHANGELOG.md b/CHANGELOG.md index 321ac0f6..3c208b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,11 @@ ### 🙏 Sponsor only features -- Title AI suggestions which you need to enable by setting the `frontMatter.sponsors.ai.titleEnabled` setting to `true`. +In this version we added a Front Matter AI which is only available for sponsors of the project. You will need to set the `frontMatter.sponsors.ai.enabled` setting to `true` to enable it. + +Once enabled, you will get the Front Matter AI help when creating new content by adding title suggestions or tag/category suggestions. + +If you want to support the project, you can do so by [becoming a sponsor](https://github.com/sponsors/estruyf). ### ✨ New features @@ -21,6 +25,7 @@ - [#530](https://github.com/estruyf/vscode-front-matter/issues/530): Implementation of the Front Matter AI 🤖 powered by [mendable.ai](https://mendable.ai) - [#537](https://github.com/estruyf/vscode-front-matter/issues/537): Allow to use the root path `/` as the public folder - [#541](https://github.com/estruyf/vscode-front-matter/issues/541): Added title AI suggestions for GitHub sponsors +- [#548](https://github.com/estruyf/vscode-front-matter/issues/548): Project selection support when working in mono-repos or multi-root workspaces - [#550](https://github.com/estruyf/vscode-front-matter/issues/550): Added taxonomy (tags/categories) AI suggestions for GitHub sponsors ### 🎨 Enhancements @@ -33,6 +38,7 @@ - [#535](https://github.com/estruyf/vscode-front-matter/issues/535): Retain the scroll position after selecting a media file - [#538](https://github.com/estruyf/vscode-front-matter/issues/538): Added support to encode emojis in the string field - [#549](https://github.com/estruyf/vscode-front-matter/issues/549): Git submodule support to sync changes +- [#554](https://github.com/estruyf/vscode-front-matter/issues/554): When inserting snippets, only the content snippets will be shown ### ⚡️ Optimizations diff --git a/enhancements.png b/enhancements.png new file mode 100644 index 00000000..0f8e9f1e Binary files /dev/null and b/enhancements.png differ diff --git a/fixes.png b/fixes.png new file mode 100644 index 00000000..c8d9af3c Binary files /dev/null and b/fixes.png differ diff --git a/new-features.png b/new-features.png new file mode 100644 index 00000000..29c039b6 Binary files /dev/null and b/new-features.png differ diff --git a/optimizations.png b/optimizations.png new file mode 100644 index 00000000..5ec4a2c0 Binary files /dev/null and b/optimizations.png differ diff --git a/package.json b/package.json index a0e56189..55c97a41 100644 --- a/package.json +++ b/package.json @@ -94,8 +94,31 @@ }] }, "configuration": { + "$id": "#gobalconfiguration", "title": "Front Matter: use frontmatter.json for shared team settings", + "type": "object", "properties": { + "frontMatter.projects": { + "type": "array", + "markdownDescription": "Specify the list of projects to load in the Front Matter CMS. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.projects)", + "default": [], + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "markdownDescription": "Specify the name of the project." + }, + "default": { + "type": "boolean", + "markdownDescription": "Specify if this project is the default project to load." + }, + "configuration": { + "$ref": "#gobalconfiguration" + } + } + } + }, "frontMatter.sponsors.ai.enabled": { "type": "boolean", "default": false, @@ -1629,6 +1652,12 @@ } }, "commands": [{ + "command": "frontMatter.project.switch", + "title": "Switch project", + "category": "Front Matter", + "icon": "$(arrow-swap)" + }, + { "command": "frontMatter.config.reload", "title": "Reload config", "category": "Front Matter" @@ -2061,6 +2090,10 @@ "command": "frontMatter.init", "when": "frontMatterCanInit" }, + { + "command": "frontMatter.project.switch", + "when": "frontMatter:project:switch:enabled" + }, { "command": "frontMatter.createTemplate", "when": "!frontMatterCanInit" @@ -2214,8 +2247,13 @@ "when": "view == frontMatter.explorer && frontMatter:has:modes == true" }, { - "command": "frontMatter.dashboard", + "command": "frontMatter.project.switch", "group": "navigation@3", + "when": "view == frontMatter.explorer && frontMatter:project:switch:enabled" + }, + { + "command": "frontMatter.dashboard", + "group": "navigation@4", "when": "view == frontMatter.explorer || view == explorer" } ] diff --git a/src/commands/Cache.ts b/src/commands/Cache.ts index e2f2b55d..0490e1c6 100644 --- a/src/commands/Cache.ts +++ b/src/commands/Cache.ts @@ -20,13 +20,15 @@ export class Cache { await Extension.getInstance().setState(key, data, type); } - private static async clear() { + public static async clear(showNotification: boolean = true) { const ext = Extension.getInstance(); await ext.setState(ExtensionState.Dashboard.Pages.Cache, undefined, 'workspace', true); await ext.setState(ExtensionState.Dashboard.Pages.Index, undefined, 'workspace', true); await ext.setState(ExtensionState.Settings.Extends, undefined, 'workspace', true); - Notifications.info('Cache cleared'); + if (showNotification) { + Notifications.info('Cache cleared'); + } } } diff --git a/src/commands/Project.ts b/src/commands/Project.ts index 457ab487..3551dc5a 100644 --- a/src/commands/Project.ts +++ b/src/commands/Project.ts @@ -1,12 +1,13 @@ import { DEFAULT_CONTENT_TYPE } from './../constants/ContentType'; import { Telemetry } from './../helpers/Telemetry'; -import { workspace, Uri } from 'vscode'; +import { workspace, Uri, commands, window } from 'vscode'; import { join } from 'path'; import { Notifications } from '../helpers/Notifications'; import { Template } from './Template'; import { Folders } from './Folders'; -import { FrameworkDetector, Logger, MediaLibrary, Settings } from '../helpers'; +import { Extension, FrameworkDetector, Logger, MediaLibrary, Settings } from '../helpers'; import { + COMMAND_NAME, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent @@ -28,6 +29,13 @@ categories: [] --- `; + public static registerCommands() { + const ext = Extension.getInstance(); + const subscriptions = ext.subscriptions; + + subscriptions.push(commands.registerCommand(COMMAND_NAME.switchProject, Project.switchProject)); + } + public static isInitialized() { const hasProjectFile = Settings.hasProjectFile(); // If it has a project file, initialize the media library @@ -74,6 +82,24 @@ categories: [] } } + public static async switchProject() { + const projects = Settings.getProjects(); + const project = await window.showQuickPick( + projects.map((p) => p.name), + { + canPickMany: false, + ignoreFocusOut: true, + title: 'Select a project to switch to' + } + ); + + if (!project) { + return; + } + + SettingsListener.switchProject(project); + } + /** * Creates the templates folder + sample if needed * @param sampleTemplate diff --git a/src/constants/Extension.ts b/src/constants/Extension.ts index e0db6fa7..36dc64ad 100644 --- a/src/constants/Extension.ts +++ b/src/constants/Extension.ts @@ -65,6 +65,9 @@ export const COMMAND_NAME = { addMissingFields: getCommandName('contenttype.addMissingFields'), setContentType: getCommandName('contenttype.setContentType'), + // Project + switchProject: getCommandName('project.switch'), + // Git gitSync: getCommandName('git.sync'), diff --git a/src/constants/ExtensionState.ts b/src/constants/ExtensionState.ts index 71ea4bf5..d3ae6c70 100644 --- a/src/constants/ExtensionState.ts +++ b/src/constants/ExtensionState.ts @@ -5,6 +5,10 @@ export const ExtensionState = { SettingPromoted: `frontMatter:Settings:Promoted`, MoveTemplatesFolder: `frontMatter:Templates:Move`, + Project: { + current: `frontMatter:Project:current` + }, + Dashboard: { Contents: { Sorting: `frontMatter:Dashboard:Contents:Sorting` diff --git a/src/constants/context.ts b/src/constants/context.ts index 06fc0e4f..3fc67cda 100644 --- a/src/constants/context.ts +++ b/src/constants/context.ts @@ -12,5 +12,7 @@ export const CONTEXT = { isSnippetsDashboardEnabled: 'frontMatter:dashboard:snippets:enabled', isDataDashboardEnabled: 'frontMatter:dashboard:data:enabled', - isGitEnabled: 'frontMatter:git:enabled' + isGitEnabled: 'frontMatter:git:enabled', + + projectSwitchEnabled: 'frontMatter:project:switch:enabled', }; diff --git a/src/constants/settings.ts b/src/constants/settings.ts index 3b6005e3..0e641015 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -99,6 +99,11 @@ export const SETTING_GIT_SUBMODULE_FOLDER = 'git.submodule.folder'; */ export const SETTING_SPONSORS_AI_ENABLED = 'sponsors.ai.enabled'; +/** + * Project override support + */ +export const SETTING_PROJECTS = 'projects'; + /** * @deprecated */ diff --git a/src/dashboardWebView/DashboardMessage.ts b/src/dashboardWebView/DashboardMessage.ts index 66539608..a1427074 100644 --- a/src/dashboardWebView/DashboardMessage.ts +++ b/src/dashboardWebView/DashboardMessage.ts @@ -5,6 +5,9 @@ export enum DashboardMessage { getMode = 'getMode', showWarning = 'showWarning', + // Project switching + switchProject = 'switchProject', + // Welcome view initializeProject = 'initializeProject', setFramework = 'setFramework', diff --git a/src/dashboardWebView/components/Header/Header.tsx b/src/dashboardWebView/components/Header/Header.tsx index 9aa05c58..eb9148ea 100644 --- a/src/dashboardWebView/components/Header/Header.tsx +++ b/src/dashboardWebView/components/Header/Header.tsx @@ -28,6 +28,7 @@ import { PaginationStatus } from './PaginationStatus'; import useThemeColors from '../../hooks/useThemeColors'; import { Startup } from './Startup'; import { Navigation } from './Navigation'; +import { ProjectSwitcher } from './ProjectSwitcher'; export interface IHeaderProps { header?: React.ReactNode; @@ -146,12 +147,14 @@ export const Header: React.FunctionComponent = ({ `bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)]` ) }`}> -
+ +
{location.pathname === routePaths.contents && ( diff --git a/src/dashboardWebView/components/Header/ProjectSwitcher.tsx b/src/dashboardWebView/components/Header/ProjectSwitcher.tsx new file mode 100644 index 00000000..514cf3ac --- /dev/null +++ b/src/dashboardWebView/components/Header/ProjectSwitcher.tsx @@ -0,0 +1,58 @@ +import { messageHandler } from '@estruyf/vscode/dist/client'; +import { Menu } from '@headlessui/react'; +import { SwitchHorizontalIcon } from '@heroicons/react/outline'; +import * as React from 'react'; +import { useRecoilValue } from 'recoil'; +import { DashboardMessage } from '../../DashboardMessage'; +import { SettingsSelector } from '../../state'; +import { MenuButton, MenuItem, MenuItems } from '../Menu'; + +export interface IProjectSwitcherProps { } + +export const ProjectSwitcher: React.FunctionComponent = (props: React.PropsWithChildren) => { + const [crntProject, setCrntProject] = React.useState(undefined); + const settings = useRecoilValue(SettingsSelector); + + const project = settings?.project; + const projects = settings?.projects || []; + + const setProject = (value: string) => { + setCrntProject(value); + messageHandler.send(DashboardMessage.switchProject, value) + } + + React.useEffect(() => { + setCrntProject(project?.name); + }, [project]); + + if (projects.length <= 1 || !crntProject) { + return null; + } + + return ( +
+ + + + project +
+ )} + title={crntProject} /> + + + {projects.map((p) => ( + setProject(p.name)} + /> + ))} + + + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Menu/MenuButton.tsx b/src/dashboardWebView/components/Menu/MenuButton.tsx index 24d73d2d..4f0b52f7 100644 --- a/src/dashboardWebView/components/Menu/MenuButton.tsx +++ b/src/dashboardWebView/components/Menu/MenuButton.tsx @@ -15,28 +15,26 @@ export const MenuButton: React.FunctionComponent = ({ disabled }: React.PropsWithChildren) => { const { getColors } = useThemeColors(); - + return ( -
- {label}: +
+
{label}:
{title} diff --git a/src/dashboardWebView/components/Menu/QuickAction.tsx b/src/dashboardWebView/components/Menu/QuickAction.tsx index 373091e9..83fc42ec 100644 --- a/src/dashboardWebView/components/Menu/QuickAction.tsx +++ b/src/dashboardWebView/components/Menu/QuickAction.tsx @@ -18,12 +18,11 @@ export const QuickAction: React.FunctionComponent = ({ type="button" title={title} onClick={onClick} - className={`px-2 group inline-flex justify-center text-sm font-medium ${ - getColors( - 'text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600', - 'text-[var(--vscode-foreground)] hover:text-[var(--vscode-list-activeSelectionForeground)]' - ) - }`} + className={`px-2 group inline-flex justify-center text-sm font-medium ${getColors( + 'text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600', + 'text-[var(--vscode-foreground)] hover:text-[var(--frontmatter-button-hoverBackground)]' + ) + }`} > {children} {title} diff --git a/src/dashboardWebView/components/SnippetsView/Snippets.tsx b/src/dashboardWebView/components/SnippetsView/Snippets.tsx index b4baa87b..2f943f2d 100644 --- a/src/dashboardWebView/components/SnippetsView/Snippets.tsx +++ b/src/dashboardWebView/components/SnippetsView/Snippets.tsx @@ -35,7 +35,14 @@ export const Snippets: React.FunctionComponent = ( const snippets = settings?.snippets || {}; const snippetKeys = useMemo(() => { - const allSnippetKeys = Object.keys(snippets).sort((a, b) => a.localeCompare(b)); + let allSnippetKeys = Object.keys(snippets).sort((a, b) => a.localeCompare(b)); + + if (viewData?.data?.filePath) { + allSnippetKeys = allSnippetKeys.filter((key) => { + return !snippets[key].isMediaSnippet; + }); + } + return allSnippetKeys.filter((key) => { const value = snippetFilter.toLowerCase(); const keyValue = key.toLowerCase(); @@ -44,7 +51,9 @@ export const Snippets: React.FunctionComponent = ( // Contains in key or description, values included in key are ranked higher (sort and fuzzy search) return keyValue.includes(value) || descriptionValue.includes(value); }); - }, [settings?.snippets, snippetFilter]); + + + }, [settings?.snippets, snippetFilter, viewData?.data?.filePath]); const onSnippetAdd = useCallback(() => { if (!snippetTitle || !snippetBody) { diff --git a/src/dashboardWebView/models/Settings.ts b/src/dashboardWebView/models/Settings.ts index 5117f173..978885e6 100644 --- a/src/dashboardWebView/models/Settings.ts +++ b/src/dashboardWebView/models/Settings.ts @@ -8,6 +8,7 @@ import { DraftField, Framework, GitSettings, + Project, Snippets, SortingSetting } from '../../models'; @@ -16,6 +17,8 @@ import { DashboardViewType } from '.'; import { DataFile } from '../../models/DataFile'; export interface Settings { + projects: Project[]; + project: Project; git: GitSettings; beta: boolean; initialized: boolean; diff --git a/src/extension.ts b/src/extension.ts index 504a14ed..cd1ba360 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -352,6 +352,9 @@ export async function activate(context: vscode.ExtensionContext) { // Cache commands Cache.registerCommands(); + // Project switching + Project.registerCommands(); + // Subscribe all commands subscriptions.push( insertTags, diff --git a/src/helpers/DashboardSettings.ts b/src/helpers/DashboardSettings.ts index 438a1196..83299da5 100644 --- a/src/helpers/DashboardSettings.ts +++ b/src/helpers/DashboardSettings.ts @@ -59,6 +59,8 @@ export class DashboardSettings { const pagination = Settings.get(SETTING_DASHBOARD_CONTENT_PAGINATION); const settings = { + projects: Settings.getProjects(), + project: Settings.getProject(), git: { isGitRepo: gitActions ? await GitListener.isGitRepository() : false, actions: gitActions || false diff --git a/src/helpers/SettingsHelper.ts b/src/helpers/SettingsHelper.ts index 680c44d6..1694988c 100644 --- a/src/helpers/SettingsHelper.ts +++ b/src/helpers/SettingsHelper.ts @@ -1,10 +1,10 @@ -import { SETTING_EXTENSIBILITY_SCRIPTS } from './../constants/settings'; +import { SETTING_EXTENSIBILITY_SCRIPTS, SETTING_PROJECTS } from './../constants/settings'; import { parseWinPath } from './parseWinPath'; import { Telemetry } from './Telemetry'; import { Notifications } from './Notifications'; import { commands, Uri, workspace, window } from 'vscode'; import * as vscode from 'vscode'; -import { ContentType, CustomTaxonomy, TaxonomyType } from '../models'; +import { ContentType, CustomTaxonomy, Project, TaxonomyType } from '../models'; import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, @@ -53,10 +53,32 @@ export class Settings { private static listeners: any[] = []; private static fileCreationWatcher: vscode.FileSystemWatcher | undefined; private static readConfigPromise: Promise | undefined = undefined; + private static project: Project | undefined = undefined; public static async init() { await Settings.readConfig(); + const projects = Settings.getProjects(); + const crntProject = await Extension.getInstance().getState( + ExtensionState.Project.current, + 'workspace' + ); + + if (projects.length > 0) { + // Get the default project + const defaultProject = projects.find((p) => { + if (crntProject) { + return p.name === crntProject; + } + return p.default; + }); + if (defaultProject) { + Settings.project = defaultProject; + } else { + Settings.project = projects[0]; + } + } + Settings.listeners = []; if (!Settings.isInitialized) { @@ -72,6 +94,45 @@ export class Settings { }); } + /** + * Get the current project + * @returns + */ + public static getProject() { + return Settings.project; + } + + /** + * Set the project + * @param value + */ + public static setProject(value: string) { + Extension.getInstance().setState(ExtensionState.Project.current, value, 'workspace'); + Settings.project = Settings.getProjects().find((p) => p.name === value); + console.log('setProject', Settings.project); + } + + /** + * Fetch all the projects + * @returns + */ + public static getProjects(): Project[] { + const settingKey = `${CONFIG_KEY}.${SETTING_PROJECTS}`; + + let projects = []; + if (Settings.globalConfig && typeof Settings.globalConfig[settingKey] !== 'undefined') { + projects = Settings.globalConfig[settingKey]; + } + + if (projects.length > 0) { + commands.executeCommand('setContext', CONTEXT.projectSwitchEnabled, true); + } else { + commands.executeCommand('setContext', CONTEXT.projectSwitchEnabled, false); + } + + return projects; + } + /** * Check if the setting is present in the workspace and ask to promote them to the global settings */ @@ -195,6 +256,16 @@ export class Settings { let setting = undefined; const settingKey = `${CONFIG_KEY}.${name}`; + if (Settings.project) { + if ( + typeof Settings.project.configuration !== 'undefined' && + typeof Settings.project.configuration[settingKey] !== 'undefined' + ) { + setting = Settings.project.configuration[settingKey]; + return setting; + } + } + if (Settings.globalConfig && typeof Settings.globalConfig[settingKey] !== 'undefined') { setting = Settings.globalConfig[settingKey]; } @@ -699,6 +770,10 @@ export class Settings { else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_CUSTOM)) { Settings.updateGlobalConfigArraySetting(SETTING_TAXONOMY_CUSTOM, 'id', configJson); } + // Projects + else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_PROJECTS)) { + Settings.updateGlobalConfigArraySetting(SETTING_PROJECTS, 'name', configJson); + } // Snippets else if ( Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_SNIPPETS) && diff --git a/src/listeners/dashboard/PagesListener.ts b/src/listeners/dashboard/PagesListener.ts index b831c891..408dd905 100644 --- a/src/listeners/dashboard/PagesListener.ts +++ b/src/listeners/dashboard/PagesListener.ts @@ -176,6 +176,7 @@ export class PagesListener extends BaseListener { this.sendMsg(DashboardCommand.searchReady, true); await this.createSearchIndex(pages); + this.sendMsg(DashboardCommand.loading, false); }); } diff --git a/src/listeners/dashboard/SettingsListener.ts b/src/listeners/dashboard/SettingsListener.ts index 10bbbe77..93d5a9a4 100644 --- a/src/listeners/dashboard/SettingsListener.ts +++ b/src/listeners/dashboard/SettingsListener.ts @@ -3,16 +3,24 @@ import { commands, Uri } from 'vscode'; import { Folders } from '../../commands/Folders'; import { COMMAND_NAME, + ExtensionState, SETTING_CONTENT_STATIC_FOLDER, SETTING_FRAMEWORK_ID, SETTING_PREVIEW_HOST } from '../../constants'; import { DashboardCommand } from '../../dashboardWebView/DashboardCommand'; import { DashboardMessage } from '../../dashboardWebView/DashboardMessage'; -import { DashboardSettings, Settings } from '../../helpers'; +import { DashboardSettings, Extension, Settings } from '../../helpers'; import { FrameworkDetector } from '../../helpers/FrameworkDetector'; import { Framework, PostMessageData } from '../../models'; import { BaseListener } from './BaseListener'; +import { Cache } from '../../commands/Cache'; +import { Preview } from '../../commands'; +import { GitListener } from '../general'; +import { DataListener } from '../panel'; +import { MarkdownFoldingProvider } from '../../providers/MarkdownFoldingProvider'; +import { ModeSwitch } from '../../services/ModeSwitch'; +import { PagesListener } from './PagesListener'; export class SettingsListener extends BaseListener { /** @@ -35,6 +43,36 @@ export class SettingsListener extends BaseListener { case DashboardMessage.addFolder: this.addFolder(msg?.payload); break; + case DashboardMessage.switchProject: + this.switchProject(msg.payload); + break; + } + } + + public static async switchProject(project: string) { + if (project) { + this.sendMsg(DashboardCommand.loading, true); + Settings.setProject(project); + await Cache.clear(false); + + // Clear out the media folder + await Extension.getInstance().setState( + ExtensionState.SelectedFolder, + undefined, + 'workspace' + ); + + Preview.init(); + GitListener.init(); + + SettingsListener.getSettings(true); + DataListener.getFoldersAndFiles(); + MarkdownFoldingProvider.triggerHighlighting(true); + ModeSwitch.register(); + + // Update pages + PagesListener.startWatchers(); + PagesListener.refresh(); } } diff --git a/src/models/Project.ts b/src/models/Project.ts new file mode 100644 index 00000000..b722d459 --- /dev/null +++ b/src/models/Project.ts @@ -0,0 +1,5 @@ +export interface Project { + name: string; + default?: boolean; + configuration: any; +} diff --git a/src/models/index.ts b/src/models/index.ts index 2f825d57..e066fc88 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -16,6 +16,7 @@ export * from './MediaPaths'; export * from './Mode'; export * from './PanelSettings'; export * from './PostMessageData'; +export * from './Project'; export * from './Snippets'; export * from './SortOrder'; export * from './SortType';