Compare commits

..

53 Commits

Author SHA1 Message Date
Elio Struyf
0ae7cb27ce Added localization 2024-03-13 14:58:58 +01:00
Elio Struyf
5e77419f5a New table implementation 2024-03-13 14:52:06 +01:00
Elio Struyf
ec9f55b982 Localization actions 2024-03-13 11:32:42 +01:00
Elio Struyf
fdcfdc971d Multi-select hook 2024-03-12 20:40:05 +01:00
Elio Struyf
2bc103026b Editor panel + content action bar 2024-03-12 13:53:43 +01:00
Elio Struyf
23b1efec55 Multi-select actions 2024-03-11 16:52:14 +01:00
Elio Struyf
2a8d7b0ebe #671 - Implement checkbox on media card 2024-03-01 08:40:05 +01:00
Elio Struyf
3b26944a4a Update changelog 2024-02-29 08:40:01 +01:00
Elio Struyf
78cac94dd6 10.1.0 2024-02-29 08:38:46 +01:00
Elio Struyf
9c6845ed8a #768 - Update data view link 2024-02-29 08:38:31 +01:00
Elio Struyf
7633ac91be Update changelog 2024-02-28 21:12:17 +01:00
Elio Struyf
282527c90d 10.0.1 2024-02-28 21:11:27 +01:00
Elio Struyf
07fbf8bdb9 #766 - Fix snippet placeholder retrieval 2024-02-28 21:11:24 +01:00
Elio Struyf
2e35da3d91 #746 - Fix in path placeholders on content creation 2024-02-28 15:54:11 +01:00
Elio Struyf
2bd607b13c Added a support.md file 2024-02-28 14:58:54 +01:00
Elio Struyf
106f1e6c94 Update script 2024-02-28 14:34:57 +01:00
Elio Struyf
54cd3ead64 Remove checkout 2024-02-28 14:32:07 +01:00
Elio Struyf
7e9bd5b0ce Add missing checkout 2024-02-28 14:29:35 +01:00
Elio Struyf
9086868817 Update action 2024-02-28 14:28:37 +01:00
Elio Struyf
4bff53299e Update action name 2024-02-28 14:26:41 +01:00
Elio Struyf
ee101cfe4d Update gh actions 2024-02-28 14:25:24 +01:00
Elio Struyf
247051f592 Updated GH action dependency 2024-02-28 14:10:44 +01:00
Elio Struyf
e6b6bba7df Updated beta release 2024-02-28 14:09:32 +01:00
Elio Struyf
be5d15d2f8 Updated changelog + date field default value 2024-02-28 11:43:36 +01:00
Elio Struyf
65364b8486 Update changelog for v10 release 2024-02-28 11:30:42 +01:00
Elio Struyf
6dd82bd4fe Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2024-02-27 09:43:51 +01:00
Elio Struyf
e0b18465dc Updated command palette commands 2024-02-27 09:43:16 +01:00
Elio Struyf
0a530dce27 Update readme 2024-02-26 15:51:40 +01:00
Elio Struyf
63e296d62f Sentry updates 2024-02-26 15:49:14 +01:00
Elio Struyf
003d93b0f2 Added telemetry information 2024-02-26 15:13:08 +01:00
Elio Struyf
59528a3db0 #746 - Slug handling when none is defined 2024-02-24 14:10:14 +01:00
Elio Struyf
c298f2fd69 #760 - Windows file path fixes in multilingual 2024-02-24 13:46:26 +01:00
Elio Struyf
b02a80c28e #760 - Add locale to recently modified panel section 2024-02-24 13:14:45 +01:00
Elio Struyf
f19bd07359 Support for using the fieldCollection field in a block field 2024-02-23 19:13:34 +01:00
Elio Struyf
83b9f2380e Date format fix 2024-02-23 16:37:15 +01:00
Elio Struyf
3f88b05a1c i10n provider for generic config 2024-02-23 09:26:41 +01:00
Elio Struyf
48ada1c352 #760 - Return locales from settings 2024-02-23 08:58:43 +01:00
Elio Struyf
a8777c4032 #760 - Type filter fix 2024-02-23 08:39:43 +01:00
Elio Struyf
fe5df3779b Merge branch 'azure-translator' into dev 2024-02-22 15:28:36 +01:00
Elio Struyf
91ec23e77c HTML text type 2024-02-22 15:28:23 +01:00
Elio Struyf
1b0a99b8fb Azure translations 2024-02-22 15:25:05 +01:00
Elio Struyf
4a0c1a4059 Update changelog 2024-02-22 13:35:14 +01:00
Elio Struyf
40c722e380 Update changelog 2024-02-22 13:30:40 +01:00
Elio Struyf
41a5e9ab7a Remove deprecated settings 2024-02-22 13:09:36 +01:00
Elio Struyf
a641aabc2a #749 - Support in extended configs 2024-02-22 11:44:23 +01:00
Elio Struyf
bc3b2c403d Update default 2024-02-22 11:15:15 +01:00
Elio Struyf
b4d2e4ea8b #749 - update name 2024-02-22 11:14:30 +01:00
Elio Struyf
b1e87d4f57 #760 - Fix to keep locale filter 2024-02-22 10:29:51 +01:00
Elio Struyf
241e660694 #756: Support for creating translation from any page + change path logic 2024-02-22 09:52:59 +01:00
Elio Struyf
03bc7e72fd Update version 2024-02-21 16:57:53 +01:00
Elio Struyf
0aca8fed16 10.0.0 2024-02-21 16:50:55 +01:00
Elio Struyf
60b5d7d759 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2024-02-21 16:50:18 +01:00
Elio Struyf
e12db5ec74 Merge pull request #759 from estruyf/i18n
New i18n feature
2024-02-21 16:49:16 +01:00
118 changed files with 2500 additions and 1146 deletions

46
.github/actions/localization/action.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Localization sync
description: Syncs the localization values from English to the other supported languages
inputs:
TRANSLATION_API_KEY:
description: 'The API key for the translation service'
required: true
TRANSLATION_API_LOCATION:
description: 'The location of the translation service'
required: true
TRANSLATION_API_URL:
description: 'The URL of the translation service'
required: true
PACKAGE_NAME:
description: 'The name of the package to be uploaded'
required: true
runs:
using: "composite"
steps:
- uses: actions/setup-node@v4
with:
node-version: 18
registry-url: https://registry.npmjs.org/
cache: 'npm'
- name: Install the dependencies
shell: bash
run: npm ci
- name: Sync localization
shell: bash
run: npm run localization:sync
env:
TRANSLATION_API_KEY: ${{ inputs.TRANSLATION_API_KEY }}
TRANSLATION_API_LOCATION: ${{ inputs.TRANSLATION_API_LOCATION }}
TRANSLATION_API_URL: ${{ inputs.TRANSLATION_API_URL }}
- name: Remove the node_modules
shell: bash
run: rm -rf node_modules
- uses: actions/upload-artifact@v4
with:
name: ${{ inputs.PACKAGE_NAME }}
path: .

View File

@@ -5,19 +5,45 @@ on:
- dev
workflow_dispatch:
env:
PACKAGE_NAME: 'fm-localized'
MS_URL: 'https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta'
VSX_URL: 'https://open-vsx.org/extension/eliostruyf/vscode-front-matter-beta'
jobs:
build:
name: 'Build and release'
localization:
name: 'Localization'
runs-on: ubuntu-latest
environment:
name: Beta
steps:
- uses: actions/checkout@v4
- name: Localize the solution
uses: ./.github/actions/localization
with:
TRANSLATION_API_KEY: ${{ secrets.TRANSLATION_API_KEY }}
TRANSLATION_API_LOCATION: ${{ secrets.TRANSLATION_API_LOCATION }}
TRANSLATION_API_URL: ${{ secrets.TRANSLATION_API_URL }}
PACKAGE_NAME: ${{ env.PACKAGE_NAME }}
release-ms:
name: 'Release to VSCode Marketplace'
runs-on: ubuntu-latest
needs: localization
environment:
name: 'MS - BETA'
url: ${{ env.MS_URL }}
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ env.PACKAGE_NAME }}
- uses: actions/setup-node@v4
with:
node-version: 18
registry-url: https://registry.npmjs.org/
cache: 'npm'
- name: Install the dependencies
run: npm ci
@@ -25,15 +51,33 @@ jobs:
- name: Prepare BETA
run: node scripts/beta-release.js $GITHUB_RUN_ID
- name: Run localization sync
run: npm run localization:sync
env:
TRANSLATION_API_KEY: ${{ secrets.TRANSLATION_API_KEY }}
TRANSLATION_API_LOCATION: ${{ secrets.TRANSLATION_API_LOCATION }}
TRANSLATION_API_URL: ${{ secrets.TRANSLATION_API_URL }}
- name: Publish
run: npx @vscode/vsce publish -p ${{ secrets.VSCE_PAT }} --baseImagesUrl https://raw.githubusercontent.com/estruyf/vscode-front-matter/dev
release-vsx:
name: 'Release to Open VSX'
runs-on: ubuntu-latest
needs: localization
environment:
name: 'Open VSX - BETA'
url: ${{ env.VSX_URL }}
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ env.PACKAGE_NAME }}
- uses: actions/setup-node@v4
with:
node-version: 18
registry-url: https://registry.npmjs.org/
cache: 'npm'
- name: Install the dependencies
run: npm ci
- name: Prepare BETA
run: node scripts/beta-release.js $GITHUB_RUN_ID
- name: Publish to open-vsx.org
run: npx ovsx publish -p ${{ secrets.OPEN_VSX_PAT }}

View File

@@ -5,19 +5,45 @@ on:
- published
workflow_dispatch:
env:
PACKAGE_NAME: 'fm-localized'
MS_URL: 'https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter'
VSX_URL: 'https://open-vsx.org/extension/eliostruyf/vscode-front-matter'
jobs:
build:
name: 'Build and release'
localization:
name: 'Localization'
runs-on: ubuntu-latest
environment:
name: Stable
steps:
- uses: actions/checkout@v4
- name: Localize the solution
uses: ./.github/actions/localization
with:
TRANSLATION_API_KEY: ${{ secrets.TRANSLATION_API_KEY }}
TRANSLATION_API_LOCATION: ${{ secrets.TRANSLATION_API_LOCATION }}
TRANSLATION_API_URL: ${{ secrets.TRANSLATION_API_URL }}
PACKAGE_NAME: ${{ env.PACKAGE_NAME }}
release-ms:
name: 'Release to VSCode Marketplace'
runs-on: ubuntu-latest
needs: localization
environment:
name: 'MS - Stable'
url: ${{ env.MS_URL }}
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ env.PACKAGE_NAME }}
- uses: actions/setup-node@v4
with:
node-version: 18
registry-url: https://registry.npmjs.org/
cache: 'npm'
- name: Install the dependencies
run: npm ci
@@ -25,15 +51,33 @@ jobs:
- name: Prepare MAIN release
run: node scripts/main-release.js
- name: Run localization sync
run: npm run localization:sync
env:
TRANSLATION_API_KEY: ${{ secrets.TRANSLATION_API_KEY }}
TRANSLATION_API_LOCATION: ${{ secrets.TRANSLATION_API_LOCATION }}
TRANSLATION_API_URL: ${{ secrets.TRANSLATION_API_URL }}
- name: Publish
run: npx @vscode/vsce publish -p ${{ secrets.VSCE_PAT }}
release-vsx:
name: 'Release to Open VSX'
runs-on: ubuntu-latest
needs: localization
environment:
name: 'Open VSX - Stable'
url: ${{ env.VSX_URL }}
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ env.PACKAGE_NAME }}
- uses: actions/setup-node@v4
with:
node-version: 18
registry-url: https://registry.npmjs.org/
cache: 'npm'
- name: Install the dependencies
run: npm ci
- name: Prepare MAIN release
run: node scripts/main-release.js
- name: Publish to open-vsx.org
run: npx ovsx publish -p ${{ secrets.OPEN_VSX_PAT }}

View File

@@ -1,12 +1,31 @@
# Change Log
## [9.5.0] - 2024-xx-xx
## [10.1.0] - 2024-xx-xx
### ✨ New features
### 🎨 Enhancements
### ⚡️ Optimizations
### 🐞 Fixes
- [#768](https://github.com/estruyf/vscode-front-matter/issues/768): Update broken link to the documentation
## [10.0.1] - 2024-02-28
### 🐞 Fixes
- [#766](https://github.com/estruyf/vscode-front-matter/issues/766): Fix for snippet placeholder retrieval
## [10.0.0] - 2024-02-28 - [Release notes](https://beta.frontmatter.codes/updates/v10.0.0)
### ✨ New features
- [#731](https://github.com/estruyf/vscode-front-matter/issues/731): Added the ability to map/unmap taxonomy to multiple pages at once
- [#746](https://github.com/estruyf/vscode-front-matter/issues/746): Placeholder support added to to the `slug` field
- [#749](https://github.com/estruyf/vscode-front-matter/issues/749): Ability to set your own filters on the content dashboard with the `frontMatter.content.filters` setting
- [#756](https://github.com/estruyf/vscode-front-matter/issues/756): i18n/multilingual content support
### 🎨 Enhancements
@@ -17,8 +36,8 @@
- [#741](https://github.com/estruyf/vscode-front-matter/issues/741): Added message on the content dashboard when content is processed
- [#747](https://github.com/estruyf/vscode-front-matter/issues/747): The `@frontmatter/extensibility` dependency now supports scripts for placeholders
- [#752](https://github.com/estruyf/vscode-front-matter/issues/752): Placeholder support in default `list` field values
### ⚡️ Optimizations
- Support for using the `fieldCollection` field in a `block` field
- Updated the list of commands which are available in the command palette
### 🐞 Fixes

View File

@@ -54,6 +54,12 @@ A couple of our extension highlights that hopefully get you interested in giving
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 10**
In version 10, we introduced the new i18n/multilingual support for your content. You can now manage your content in multiple languages, more information can be found in the [multilingual](https://frontmatter.codes/docs/content-creation/multilingual) section of our documentation.
![Multilingual support](https://beta.frontmatter.codes/releases/v10.0.0/multilingual-content.png)
**Version 9**
The extension is now available in multiple languages: English, German, and Japanese. Want to add your language? Check out the [localization the extension](https://frontmatter.codes/docs/contributing#translating-the-extension).
@@ -187,6 +193,27 @@ You can open showcase issues for the following things:
</a>
</p>
## 📊 Telemetry
The Front Matter CMS extension collects telemetry data to help us build a better understand which features from the CMS are used. The extension respects the `telemetry.enableTelemetry` setting which you can learn more about in the [Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry), or you can only disable it for the extension by configuring the `frontMatter.telemetry.disable` setting.
We only collect the following data:
- Type of event
- Extension title (main or beta)
- Extension version
No user-specific data is collected, you can check the telemetry implementation in the following files:
- [Telemetry class](https://github.com/estruyf/vscode-front-matter/blob/59528a3db01be8d34dc40638e6cf827090e31986/src/helpers/Telemetry.ts)
- [Metrics API](https://github.com/FrontMatter/web-documentation-nextjs/blob/main/pages/api/metrics.ts)
For crash reports in the webviews, we make use of Sentry to help us understand what went wrong. This data is only used to fix issues and improve the extension. You can find more information about the Sentry implementation in the following files:
- [Sentry config](https://github.com/estruyf/vscode-front-matter/blob/63e296d62f11be73ac86d9e823084247952a7ddc/src/utils/sentryInit.ts)
> The user ip address is not collected.
## 🔑 License
[MIT](./LICENSE)

View File

@@ -52,6 +52,12 @@ A couple of our extension highlights that hopefully get you interested in giving
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 10**
In version 10, we introduced the new i18n/multilingual support for your content. You can now manage your content in multiple languages, more information can be found in the [multilingual](https://frontmatter.codes/docs/content-creation/multilingual) section of our documentation.
![Multilingual support](https://beta.frontmatter.codes/releases/v10.0.0/multilingual-content.png)
**Version 9**
The extension is now available in multiple languages: English, German, and Japanese. Want to add your language? Check out the [localization the extension](https://frontmatter.codes/docs/contributing#translating-the-extension).
@@ -193,6 +199,27 @@ You can open showcase issues for the following things:
</a>
</p>
## 📊 Telemetry
The Front Matter CMS extension collects telemetry data to help us build a better understand which features from the CMS are used. The extension respects the `telemetry.enableTelemetry` setting which you can learn more about in the [Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry), or you can only disable it for the extension by configuring the `frontMatter.telemetry.disable` setting.
We only collect the following data:
- Type of event
- Extension title (main or beta)
- Extension version
No user-specific data is collected, you can check the telemetry implementation in the following files:
- [Telemetry class](https://github.com/estruyf/vscode-front-matter/blob/59528a3db01be8d34dc40638e6cf827090e31986/src/helpers/Telemetry.ts)
- [Metrics API](https://github.com/FrontMatter/web-documentation-nextjs/blob/main/pages/api/metrics.ts)
For crash reports in the webviews, we make use of Sentry to help us understand what went wrong. This data is only used to fix issues and improve the extension. You can find more information about the Sentry implementation in the following files:
- [Sentry config](https://github.com/estruyf/vscode-front-matter/blob/63e296d62f11be73ac86d9e823084247952a7ddc/src/utils/sentryInit.ts)
> The user ip address is not collected.
## 🔑 License
[MIT](./LICENSE)

17
SUPPORT.md Normal file
View File

@@ -0,0 +1,17 @@
# Support
This article provides information on how to get support for Front Matter CMS.
> 👉 Note: before participating in our community, please read our [code of conduct](./CODE_OF_CONDUCT.md). By interacting with this repository, organization, or community you agree to abide by its terms.
## Asking for help
There are a few different ways to ask for help with Front Matter CMS:
1. **GitHub Discussions**: You can ask questions and share your experiences in the [Discussions](https://github.com/estruyf/vscode-front-matter/discussions) section of this repository.
2. **GitHub Issues**: If you encounter a bug or have a feature request, you can open an issue in the [Issues](https://github.com/estruyf/vscode-front-matter/issues) section of this repository.
3. **Discord**: You can join our [Discord](https://discord.gg/JBVtNMsJFB) server and ask your questions there.
## Contributing
If you would like to contribute to Front Matter CMS, please read our [contributing guide](./CONTRIBUTING.md).

View File

@@ -247,14 +247,6 @@
background-color: var(--vscode-button-secondaryHoverBackground);
}
.table__cell {
overflow: hidden;
}
.table__title {
text-transform: capitalize;
}
.table__cell__seo_details {
padding: 10px;
}
@@ -281,11 +273,6 @@
margin-left: 0.5rem;
}
.seo__status__note {
font-size: 10px;
padding: 3px 0;
}
/* Fields */
.field__toggle {
position: relative;
@@ -364,7 +351,7 @@ input:checked + .field__toggle__slider:before {
}
/* File list */
.file_list vscode-label {
.file_list label {
border-bottom: 1px solid var(--vscode-foreground);
}

View File

@@ -37,6 +37,10 @@
"common.back": "Back",
"common.open": "Open",
"common.openWithValue": "Open: {0}",
"common.view": "View",
"common.translate": "Translate",
"common.languages": "Languages",
"common.scripts": "Scripts",
"loading.initPages": "Loading content",
@@ -67,8 +71,14 @@
"settings.commonSettings.startCommand": "SSG/Framework start command",
"settings.integrationsView.deepl.title": "DeepL",
"settings.integrationsView.deepl.intput.label": "Authentication key",
"settings.integrationsView.deepl.intput.placeholder": "Enter your DeepL authentication key",
"settings.integrationsView.deepl.intput.label": "API key",
"settings.integrationsView.deepl.intput.placeholder": "Enter your Azure Translator API key",
"settings.integrationsView.azure.title": "Azure AI Translator Service",
"settings.integrationsView.azure.intput.label": "Subscription key",
"settings.integrationsView.azure.intput.placeholder": "Enter your Azure AI Translator - Subscription key",
"settings.integrationsView.azure.region.label": "Region",
"settings.integrationsView.azure.region.placeholder": "Enter your Azure AI Translator - Region. Example: westeurope",
"developer.title": "Developer mode",
"developer.reload.title": "Reload the dashboard",
@@ -140,6 +150,10 @@
"dashboard.filters.languageFilter.label": "Locale",
"dashboard.filters.languageFilter.all": "All",
"dashboard.header.actionsBar.itemsSelected": "{0} selected",
"dashboard.header.actionsBar.alertDelete.title": "Delete selected files",
"dashboard.header.actionsBar.alertDelete.description": "Are you sure you want to delete the selected files?",
"dashboard.header.breadcrumb.home": "Home",
"dashboard.header.clearFilters.title": "Clear filters, grouping, and sorting",
@@ -223,6 +237,9 @@
"dashboard.media.folderCreation.hexo.create": "Create post asset folder",
"dashboard.media.folderCreation.folder.create": "Create new folder",
"dashboard.media.folderItem.contentDirectory": "Content directory",
"dashboard.media.folderItem.publicDirectory": "Public directory",
"dashboard.media.item.buttom.insert.image": "Insert image",
"dashboard.media.item.buttom.insert.snippet": "Insert snippet",
@@ -539,7 +556,9 @@
"commands.i18n.create.warning.noFile": "The file could not be retrieved.",
"commands.i18n.create.warning.noContentType": "Content type could not be retrieved for the current file.",
"commands.i18n.create.warning.noConfig": "No i18n configuration found.",
"commands.i18n.create.warning.notDefaultLocale": "The current file cannot be used for i18n content creation.",
"commands.i18n.create.error.noLocaleDefinition": "Could not retrieve the locale for the current file.",
"commands.i18n.create.error.noLocales": "Current file has been translated to all available languages.",
"commands.i18n.create.error.noContentFolder": "Could not define a content folder for the current file.",
"commands.i18n.create.error.fileExists": "The i18n translation already exists.",
"commands.i18n.create.success.created": "Created \"{0}\" i18n content file.",
"commands.i18n.create.quickPick.title": "Create content for locale",
@@ -656,9 +675,6 @@
"helpers.extension.getVersion.changelog": "Check the changelog",
"helpers.extension.getVersion.starIt": "Give it a ⭐️",
"helpers.extension.getVersion.update.notification": "{0} has been updated to v{1} — check out what's new!",
"helpers.extension.migrateSettings.deprecated.warning": "The \"{0}\" and \"{1}\" settings have been deprecated. Please use the \"isPublishDate\" and \"isModifiedDate\" datetime field properties instead.",
"helpers.extension.migrateSettings.deprecated.warning.hide": "Hide",
"helpers.extension.migrateSettings.deprecated.warning.seeGuide": "See migration guide",
"helpers.extension.migrateSettings.templates.quickPick.title": "{0} - Templates",
"helpers.extension.migrateSettings.templates.quickPick.placeholder": "Do you want to keep on using the template functionality?",
"helpers.extension.checkIfExtensionCanRun.warning": "Front Matter BETA cannot be used while the stable version is installed. Please ensure that you have only over version installed.",
@@ -693,6 +709,7 @@
"helpers.questions.selectContentType.quickPick.title": "Content type",
"helpers.questions.selectContentType.quickPick.placeholder": "Select the content type to create your new content",
"helpers.questions.selectContentType.noSelection.warning": "No content type was selected.",
"helpers.questions.selectContentType.quickPick.error.noContentTypes": "There are no matching content types configured for this folder.",
"helpers.seoHelper.checkLength.diagnostic.message": "Article {0} is longer than {1} characters (current length: {2}). For SEO reasons, it would be better to make it less than {1} characters.",

87
package-lock.json generated
View File

@@ -1,19 +1,18 @@
{
"name": "vscode-front-matter-beta",
"version": "9.5.0",
"version": "10.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vscode-front-matter-beta",
"version": "9.5.0",
"version": "10.1.0",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dropdown-menu": "^2.0.6"
},
"devDependencies": {
"@actions/core": "^1.10.0",
"@bendera/vscode-webview-elements": "0.6.2",
"@estruyf/vscode": "^1.1.0",
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1",
@@ -38,6 +37,7 @@
"@types/vscode": "^1.73.0",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@vscode-elements/elements": "^1.2.0",
"@vscode/l10n": "^0.0.14",
"@vscode/webview-ui-toolkit": "^1.2.2",
"@webpack-cli/serve": "^1.7.0",
@@ -85,7 +85,7 @@
"react-quill": "^2.0.0",
"react-router-dom": "^6.8.0",
"react-sortable-hoc": "^2.0.0",
"recoil": "^0.4.1",
"recoil": "^0.7.7",
"remark-gfm": "^3.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.8",
@@ -400,15 +400,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@bendera/vscode-webview-elements": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/@bendera/vscode-webview-elements/-/vscode-webview-elements-0.6.2.tgz",
"integrity": "sha512-smtr+KvCKV2MwjVrmyvrhonpaXVpxCjTMXUQOwDwWSAQ42x5pnlpjCGElz2dljc5VHS1Mh1ovPSQ/P3jAm7vMQ==",
"dev": true,
"dependencies": {
"lit-element": "^2.5.1"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
@@ -764,6 +755,21 @@
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
"dev": true
},
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz",
"integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==",
"dev": true
},
"node_modules/@lit/reactive-element": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz",
"integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==",
"dev": true,
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0"
}
},
"node_modules/@microsoft/fast-element": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.12.0.tgz",
@@ -2073,6 +2079,12 @@
"integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==",
"dev": true
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true
},
"node_modules/@types/uglify-js": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.4.tgz",
@@ -2337,6 +2349,15 @@
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
"dev": true
},
"node_modules/@vscode-elements/elements": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@vscode-elements/elements/-/elements-1.2.0.tgz",
"integrity": "sha512-aCsf9iEnx+PE2rRfAySjvFTSgqP4NUvHG0nOc5AxFB1FXHyG/ayYA2TN9XpT7zuO024tRAu+XoKREbRC7uAmLA==",
"dev": true,
"dependencies": {
"lit": "^3.1.2"
}
},
"node_modules/@vscode/l10n": {
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.14.tgz",
@@ -6574,20 +6595,36 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
"node_modules/lit-element": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.5.1.tgz",
"integrity": "sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==",
"node_modules/lit": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lit/-/lit-3.1.2.tgz",
"integrity": "sha512-VZx5iAyMtX7CV4K8iTLdCkMaYZ7ipjJZ0JcSdJ0zIdGxxyurjIn7yuuSxNBD7QmjvcNJwr0JS4cAdAtsy7gZ6w==",
"dev": true,
"dependencies": {
"lit-html": "^1.1.1"
"@lit/reactive-element": "^2.0.4",
"lit-element": "^4.0.4",
"lit-html": "^3.1.2"
}
},
"node_modules/lit-element": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.4.tgz",
"integrity": "sha512-98CvgulX6eCPs6TyAIQoJZBCQPo80rgXR+dVBs61cstJXqtI+USQZAbA4gFHh6L/mxBx9MrgPLHLsUgDUHAcCQ==",
"dev": true,
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0",
"@lit/reactive-element": "^2.0.4",
"lit-html": "^3.1.2"
}
},
"node_modules/lit-html": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.4.1.tgz",
"integrity": "sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA==",
"dev": true
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.2.tgz",
"integrity": "sha512-3OBZSUrPnAHoKJ9AMjRL/m01YJxQMf+TMHanNtTHG68ubjnZxK0RFl102DPzsw4mWnHibfZIBJm3LWCZ/LmMvg==",
"dev": true,
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/load-json-file": {
"version": "4.0.0",
@@ -10193,9 +10230,9 @@
}
},
"node_modules/recoil": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/recoil/-/recoil-0.4.1.tgz",
"integrity": "sha512-vp6KPwlHOjJ4bJofmdDchmgI9ilMTCoUisK8/WYLl8dThH7e7KmtZttiLgvDb2Em99dUfTEsk8vT8L1nUMgqXQ==",
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz",
"integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==",
"dev": true,
"dependencies": {
"hamt_plus": "1.0.2"

View File

@@ -3,14 +3,15 @@
"displayName": "Front Matter CMS",
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Docusaurus, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "9.5.0",
"version": "10.1.0",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
"color": "#0e131f",
"theme": "dark"
},
"badges": [{
"badges": [
{
"description": "version",
"url": "https://img.shields.io/github/package-json/v/estruyf/vscode-front-matter?color=green&label=vscode-front-matter&style=flat-square",
"href": "https://github.com/estruyf/vscode-front-matter"
@@ -70,7 +71,8 @@
"**/.frontmatter/config/*.json": "jsonc"
}
},
"keybindings": [{
"keybindings": [
{
"command": "frontMatter.dashboard",
"key": "alt+d"
},
@@ -88,19 +90,23 @@
}
],
"viewsContainers": {
"activitybar": [{
"id": "frontmatter-explorer",
"title": "FM",
"icon": "$(fm-logo)"
}]
"activitybar": [
{
"id": "frontmatter-explorer",
"title": "FM",
"icon": "$(fm-logo)"
}
]
},
"views": {
"frontmatter-explorer": [{
"id": "frontMatter.explorer",
"name": "Front Matter",
"icon": "$(fm-logo)",
"type": "webview"
}]
"frontmatter-explorer": [
{
"id": "frontMatter.explorer",
"name": "Front Matter",
"icon": "$(fm-logo)",
"type": "webview"
}
]
},
"configuration": {
"title": "%settings.configuration.title%",
@@ -168,7 +174,8 @@
"frontMatter.content.defaultFileType": {
"type": "string",
"default": "md",
"oneOf": [{
"oneOf": [
{
"enum": [
"md",
"mdx"
@@ -184,7 +191,8 @@
"frontMatter.content.defaultSorting": {
"type": "string",
"default": "",
"oneOf": [{
"oneOf": [
{
"enum": [
"LastModifiedAsc",
"LastModifiedDesc",
@@ -347,7 +355,8 @@
},
"additionalProperties": false,
"required": [
"locale"
"locale",
"path"
]
},
"scope": "Content"
@@ -526,13 +535,19 @@
"frontMatter.content.filters": {
"type": "array",
"default": [
"pageFolders",
"contentFolders",
"tags",
"categories"
],
"markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%",
"items": [{
"type": "string"
"items": [
{
"type": "string",
"enum": [
"contentFolders",
"tags",
"categories"
]
},
{
"type": "object",
@@ -599,7 +614,8 @@
"command": {
"$id": "#scriptCommand",
"type": "string",
"anyOf": [{
"anyOf": [
{
"enum": [
"node",
"bash",
@@ -701,17 +717,6 @@
"default": "",
"markdownDescription": "%setting.frontMatter.dashboard.content.card.fields.title.markdownDescription%"
},
"frontMatter.dashboard.mediaSnippet": {
"type": "array",
"default": [],
"markdownDescription": "%setting.frontMatter.dashboard.mediaSnippet.markdownDescription%",
"deprecationMessage": "%setting.frontMatter.dashboard.mediaSnippet.deprecationMessage%",
"items": {
"type": "string",
"description": "%setting.frontMatter.dashboard.mediaSnippet.items.description%"
},
"scope": "dashboard"
},
"frontMatter.dashboard.openOnStart": {
"type": [
"boolean",
@@ -806,7 +811,8 @@
"title",
"file"
],
"anyOf": [{
"anyOf": [
{
"required": [
"schema"
]
@@ -860,7 +866,8 @@
"id",
"path"
],
"anyOf": [{
"anyOf": [
{
"required": [
"schema"
]
@@ -1101,26 +1108,29 @@
}
}
},
"default": [{
"name": "default",
"fileTypes": null,
"fields": [{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Caption",
"name": "caption",
"type": "string"
},
{
"title": "Alt text",
"name": "alt",
"type": "string"
}
]
}],
"default": [
{
"name": "default",
"fileTypes": null,
"fields": [
{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Caption",
"name": "caption",
"type": "string"
},
{
"title": "Alt text",
"name": "alt",
"type": "string"
}
]
}
],
"scope": "Media"
},
"frontMatter.media.supportedMimeTypes": {
@@ -1350,7 +1360,8 @@
"default": "",
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%",
"not": {
"anyOf": [{
"anyOf": [
{
"const": ""
},
{
@@ -1544,7 +1555,8 @@
"type",
"name"
],
"allOf": [{
"allOf": [
{
"if": {
"properties": {
"type": {
@@ -1752,48 +1764,51 @@
"fields"
]
},
"default": [{
"name": "default",
"pageBundle": false,
"fields": [{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Description",
"name": "description",
"type": "string"
},
{
"title": "Publishing date",
"name": "date",
"type": "datetime",
"default": "{{now}}",
"isPublishDate": true
},
{
"title": "Content preview",
"name": "preview",
"type": "image"
},
{
"title": "Is in draft",
"name": "draft",
"type": "boolean"
},
{
"title": "Tags",
"name": "tags",
"type": "tags"
},
{
"title": "Categories",
"name": "categories",
"type": "categories"
}
]
}],
"default": [
{
"name": "default",
"pageBundle": false,
"fields": [
{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Description",
"name": "description",
"type": "string"
},
{
"title": "Publishing date",
"name": "date",
"type": "datetime",
"default": "{{now}}",
"isPublishDate": true
},
{
"title": "Content preview",
"name": "preview",
"type": "image"
},
{
"title": "Is in draft",
"name": "draft",
"type": "boolean"
},
{
"title": "Tags",
"name": "tags",
"type": "tags"
},
{
"title": "Categories",
"name": "categories",
"type": "categories"
}
]
}
],
"scope": "Taxonomy"
},
"frontMatter.taxonomy.customTaxonomy": {
@@ -1806,7 +1821,8 @@
"type": "string",
"description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%",
"not": {
"anyOf": [{
"anyOf": [
{
"const": ""
},
{
@@ -1834,12 +1850,6 @@
},
"scope": "Taxonomy"
},
"frontMatter.taxonomy.dateField": {
"type": "string",
"default": "date",
"markdownDescription": "%setting.frontMatter.taxonomy.dateField.markdownDescription%",
"deprecationMessage": "%setting.frontMatter.taxonomy.dateField.deprecationMessage%"
},
"frontMatter.taxonomy.dateFormat": {
"type": "string",
"markdownDescription": "%setting.frontMatter.taxonomy.dateFormat.markdownDescription%",
@@ -1893,12 +1903,6 @@
"markdownDescription": "%setting.frontMatter.taxonomy.indentArrays.markdownDescription%",
"scope": "Taxonomy"
},
"frontMatter.taxonomy.modifiedField": {
"type": "string",
"default": "lastmod",
"markdownDescription": "%setting.frontMatter.taxonomy.modifiedField.markdownDescription%",
"deprecationMessage": "%setting.frontMatter.taxonomy.modifiedField.deprecationMessage%"
},
"frontMatter.taxonomy.quoteStringValues": {
"type": "boolean",
"default": false,
@@ -2003,7 +2007,8 @@
}
}
},
"commands": [{
"commands": [
{
"command": "frontMatter.project.switch",
"title": "%command.frontMatter.project.switch%",
"category": "Front Matter",
@@ -2329,16 +2334,21 @@
}
}
],
"submenus": [{
"id": "frontmatter.submenu",
"label": "Front Matter"
}],
"submenus": [
{
"id": "frontmatter.submenu",
"label": "Front Matter"
}
],
"menus": {
"webview/context": [{
"command": "workbench.action.webview.openDeveloperTools",
"when": "frontMatter:isDevelopment"
}],
"editor/title": [{
"webview/context": [
{
"command": "workbench.action.webview.openDeveloperTools",
"when": "frontMatter:isDevelopment"
}
],
"editor/title": [
{
"command": "frontMatter.markup.heading",
"group": "navigation@-133",
"when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg"
@@ -2371,7 +2381,7 @@
{
"command": "frontMatter.i18n.create",
"group": "navigation@-127",
"when": "frontMatter:file:isValid && frontMatter:i18n:default"
"when": "frontMatter:file:isValid && frontMatter:i18n:enabled"
},
{
"command": "frontMatter.markup.options",
@@ -2424,11 +2434,14 @@
"when": "resourceFilename == 'frontmatter.json'"
}
],
"explorer/context": [{
"submenu": "frontmatter.submenu",
"group": "frontmatter@1"
}],
"frontmatter.submenu": [{
"explorer/context": [
{
"submenu": "frontmatter.submenu",
"group": "frontmatter@1"
}
],
"frontmatter.submenu": [
{
"command": "frontMatter.createFromTemplate",
"when": "explorerResourceIsFolder",
"group": "frontmatter@1"
@@ -2444,7 +2457,8 @@
"group": "frontmatter@3"
}
],
"commandPalette": [{
"commandPalette": [
{
"command": "frontMatter.init",
"when": "frontMatterCanInit"
},
@@ -2466,7 +2480,7 @@
},
{
"command": "frontMatter.i18n.create",
"when": "frontMatter:i18n:default"
"when": "frontMatter:i18n:enabled"
},
{
"command": "frontMatter.authenticate",
@@ -2476,6 +2490,10 @@
"command": "frontMatter.collapseSections",
"when": "false"
},
{
"command": "frontMatter.remap",
"when": "false"
},
{
"command": "frontMatter.insertTags",
"when": "false"
@@ -2560,6 +2578,22 @@
"command": "frontMatter.createTemplate",
"when": "false"
},
{
"command": "frontMatter.contenttype.addMissingFields",
"when": "false"
},
{
"command": "frontMatter.exportTaxonomy",
"when": "false"
},
{
"command": "frontMatter.generateSlug",
"when": "false"
},
{
"command": "frontMatter.promoteSettings",
"when": "false"
},
{
"command": "frontMatter.insertSnippet",
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
@@ -2596,16 +2630,13 @@
"command": "frontMatter.contenttype.generate",
"when": "frontMatter:file:isValid == true"
},
{
"command": "frontMatter.contenttype.addMissingFields",
"when": "frontMatter:file:isValid == true"
},
{
"command": "frontMatter.contenttype.setContentType",
"when": "frontMatter:file:isValid == true"
}
],
"view/title": [{
"view/title": [
{
"command": "frontMatter.chatbot",
"group": "navigation@0",
"when": "view == frontMatter.explorer"
@@ -2637,52 +2668,57 @@
}
]
},
"grammars": [{
"path": "./syntaxes/hugo.tmLanguage.json",
"scopeName": "frontmatter.markdown.hugo",
"injectTo": [
"text.html.markdown"
]
}],
"walkthroughs": [{
"id": "frontmatter.welcome",
"title": "Get started with Front Matter",
"description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.",
"steps": [{
"id": "frontmatter.welcome.init",
"title": "Get started",
"description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)",
"media": {
"markdown": "assets/walkthrough/get-started.md"
"grammars": [
{
"path": "./syntaxes/hugo.tmLanguage.json",
"scopeName": "frontmatter.markdown.hugo",
"injectTo": [
"text.html.markdown"
]
}
],
"walkthroughs": [
{
"id": "frontmatter.welcome",
"title": "Get started with Front Matter",
"description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.",
"steps": [
{
"id": "frontmatter.welcome.init",
"title": "Get started",
"description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)",
"media": {
"markdown": "assets/walkthrough/get-started.md"
},
"completionEvents": [
"onContext:frontMatterInitialized"
]
},
"completionEvents": [
"onContext:frontMatterInitialized"
]
},
{
"id": "frontmatter.welcome.documentation",
"title": "Documentation",
"description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)",
"media": {
"markdown": "assets/walkthrough/documentation.md"
{
"id": "frontmatter.welcome.documentation",
"title": "Documentation",
"description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)",
"media": {
"markdown": "assets/walkthrough/documentation.md"
},
"completionEvents": [
"onLink:https://frontmatter.codes/docs"
]
},
"completionEvents": [
"onLink:https://frontmatter.codes/docs"
]
},
{
"id": "frontmatter.welcome.supporter",
"title": "Support the project",
"description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)",
"media": {
"markdown": "assets/walkthrough/support-the-project.md"
},
"completionEvents": [
"onLink:https://github.com/sponsors/estruyf"
]
}
]
}]
{
"id": "frontmatter.welcome.supporter",
"title": "Support the project",
"description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)",
"media": {
"markdown": "assets/walkthrough/support-the-project.md"
},
"completionEvents": [
"onLink:https://github.com/sponsors/estruyf"
]
}
]
}
]
},
"scripts": {
"dev:ext": "npm run clean && npm run localization:generate && npm-run-all --parallel watch:*",
@@ -2708,7 +2744,6 @@
},
"devDependencies": {
"@actions/core": "^1.10.0",
"@bendera/vscode-webview-elements": "0.6.2",
"@estruyf/vscode": "^1.1.0",
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1",
@@ -2733,6 +2768,7 @@
"@types/vscode": "^1.73.0",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@vscode-elements/elements": "^1.2.0",
"@vscode/l10n": "^0.0.14",
"@vscode/webview-ui-toolkit": "^1.2.2",
"@webpack-cli/serve": "^1.7.0",
@@ -2780,7 +2816,7 @@
"react-quill": "^2.0.0",
"react-router-dom": "^6.8.0",
"react-sortable-hoc": "^2.0.0",
"recoil": "^0.4.1",
"recoil": "^0.7.7",
"remark-gfm": "^3.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.8",
@@ -2811,4 +2847,4 @@
"dependencies": {
"@radix-ui/react-dropdown-menu": "^2.0.6"
}
}
}

View File

@@ -15,7 +15,7 @@
"command.frontMatter.initTemplate": "Initialize the template folder",
"command.frontMatter.createTemplate": "Create template from current file",
"command.frontMatter.createCategory": "Create category",
"command.frontMatter.createContent": "Create new content from defined content type or template",
"command.frontMatter.createContent": "Create new content",
"command.frontMatter.createTag": "Create tag",
"command.frontMatter.diagnostics": "Diagnostic logging",
"command.frontMatter.exportTaxonomy": "Export all tags & categories to your settings",

View File

@@ -10,7 +10,8 @@ import {
SETTING_SLUG_PREFIX,
SETTING_SLUG_SUFFIX,
SETTING_CONTENT_PLACEHOLDERS,
TelemetryEvent
TelemetryEvent,
SETTING_SLUG_TEMPLATE
} from './../constants';
import * as vscode from 'vscode';
import { CustomPlaceholder, Field } from '../models';
@@ -115,9 +116,10 @@ export class Article {
}
const cloneArticle = Object.assign({}, article);
const dateField = ArticleHelper.getModifiedDateField(article) || DefaultFields.LastModified;
const dateField = ArticleHelper.getModifiedDateField(article);
try {
cloneArticle.data[dateField] = Article.formatDate(new Date());
const fieldName = dateField?.name || DefaultFields.LastModified;
cloneArticle.data[fieldName] = Article.formatDate(new Date(), dateField?.dateFormat);
return cloneArticle;
} catch (e: unknown) {
Notifications.error(
@@ -260,6 +262,21 @@ export class Article {
return;
}
const slugTemplate = Settings.get<string>(SETTING_SLUG_TEMPLATE);
if (slugTemplate) {
if (slugTemplate === '{{title}}') {
const article = ArticleHelper.getFrontMatter(editor);
if (article?.data?.title) {
return article.data.title.toLowerCase().replace(/\s/g, '-');
}
} else {
const article = ArticleHelper.getFrontMatter(editor);
if (article?.data) {
return SlugHelper.createSlug(article.data.title, article.data, slugTemplate);
}
}
}
const file = parseWinPath(editor.document.fileName);
if (!isValidFile(file)) {

View File

@@ -358,7 +358,7 @@ export class Dashboard {
version.usedVersion ? '' : `data-showWelcome="true"`
} ${
experimental ? `data-experimental="${experimental}"` : ''
} data-webview-url="${webviewUrl}" ></div>
} data-webview-url="${webviewUrl}" data-is-crash-disabled="${!Telemetry.isVscodeEnabled()}" ></div>
${(scriptsToLoad || [])
.map((script) => {

View File

@@ -99,7 +99,7 @@ export class Folders {
}
const folders = Folders.get().filter((f) => !f.disableCreation);
const location = folders.find((f) => f.title === selectedFolder);
const location = folders.find((f) => f.path === selectedFolder.path);
if (location) {
const folderPath = Folders.getFolderPath(Uri.file(location.path));
if (folderPath) {
@@ -293,31 +293,6 @@ export class Folders {
if (crntFolderInfo) {
folderInfo.push(crntFolderInfo);
}
// Process localization folders
if (folder.defaultLocale) {
const i18nConfig = folder.locales || Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
if (i18nConfig) {
for (const i18n of i18nConfig) {
if (i18n.locale !== folder.defaultLocale && i18n.path) {
const i18nFolder = {
...folder,
path: join(folder.path, i18n.path),
title: `${folder.title} (${i18n.title})`
} as ContentFolder;
const crntFolderInfo = await Folders.getFilesByFolder(
i18nFolder,
supportedFiles,
limit
);
if (crntFolderInfo) {
folderInfo.push(crntFolderInfo);
}
}
}
}
}
}
return folderInfo;
@@ -333,11 +308,14 @@ export class Folders {
public static get(): ContentFolder[] {
const wsFolder = Folders.getWorkspaceFolder();
let folders: ContentFolder[] = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[];
const i18nSettings = Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
// Filter out folders without a path
folders = folders.filter((f) => f.path);
const contentFolders = folders.map((folder) => {
const contentFolders: ContentFolder[] = [];
folders.forEach((folder) => {
if (!folder.title) {
folder.title = basename(folder.path);
}
@@ -371,11 +349,52 @@ export class Folders {
}
}
return {
...folder,
originalPath: folder.path,
path: folderPath
};
// Check i18n
if (folder.defaultLocale && (folder.locales || i18nSettings)) {
const i18nConfig =
folder.locales && folder.locales.length > 0 ? folder.locales : i18nSettings;
let defaultLocale;
let sourcePath = folderPath;
let localeFolders: ContentFolder[] = [];
if (i18nConfig && i18nConfig.length > 0) {
for (const i18n of i18nConfig) {
if (i18n.locale === folder.defaultLocale) {
defaultLocale = i18n;
} else if (i18n.locale !== folder.defaultLocale && i18n.path) {
localeFolders.push({
...folder,
title: folder.title,
originalPath: folder.path,
locale: i18n.locale,
localeTitle: i18n?.title || i18n.locale,
localeSourcePath: sourcePath,
path: parseWinPath(join(folderPath, i18n.path))
});
}
}
}
contentFolders.push({
...folder,
title: folder.title,
locale: folder.defaultLocale,
localeTitle: defaultLocale?.title || folder.defaultLocale,
originalPath: folder.path,
localeSourcePath: sourcePath,
path: parseWinPath(join(folderPath, defaultLocale?.path || ''))
});
contentFolders.push(...localeFolders);
} else {
contentFolders.push({
...folder,
locale: folder.defaultLocale,
originalPath: folder.path,
path: folderPath
});
}
});
return contentFolders.filter((folder) => folder !== null) as ContentFolder[];
@@ -651,7 +670,9 @@ export class Folders {
return {
title: folder.title,
files: files.length,
lastModified: fileStats
lastModified: fileStats,
locale: folder.locale,
localeTitle: folder.localeTitle
};
}
}

View File

@@ -44,8 +44,8 @@ export class StatusListener {
commands.executeCommand('setContext', CONTEXT.isValidFile, true);
// Check i18n
const isI18nDefault = await i18n.isDefaultLanguage(document.uri.fsPath);
commands.executeCommand('setContext', CONTEXT.isI18nDefault, isI18nDefault);
const isI18nEnabled = await i18n.isLocaleEnabled(document.uri.fsPath);
commands.executeCommand('setContext', CONTEXT.isI18nEnabled, isI18nEnabled);
const article = editor
? ArticleHelper.getFrontMatter(editor)
@@ -88,7 +88,7 @@ export class StatusListener {
}
} else {
commands.executeCommand('setContext', CONTEXT.isValidFile, false);
commands.executeCommand('setContext', CONTEXT.isI18nDefault, false);
commands.executeCommand('setContext', CONTEXT.isI18nEnabled, false);
const panel = PanelProvider.getInstance();
if (panel && panel.visible) {

View File

@@ -4,13 +4,12 @@ import {
ContentType,
Extension,
FrameworkDetector,
Logger,
Notifications,
Settings,
openFileInEditor,
parseWinPath
} from '../helpers';
import { COMMAND_NAME, ExtensionState, SETTING_CONTENT_I18N } from '../constants';
import { COMMAND_NAME, SETTING_CONTENT_I18N } from '../constants';
import { ContentFolder, Field, I18nConfig, ContentType as IContentType } from '../models';
import { join, parse } from 'path';
import { existsAsync } from '../utils';
@@ -19,6 +18,7 @@ import { ParsedFrontMatter } from '../parsers';
import { PagesListener } from '../listeners/dashboard';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../localization';
import { Translations } from '../services/Translations';
export class i18n {
private static processedFiles: {
@@ -43,6 +43,30 @@ export class i18n {
i18n.processedFiles = {};
}
/**
* Retrieves all the I18nConfig settings.
*
* @returns An array of I18nConfig settings.
*/
public static getAll() {
const i18nSettings = Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N) || [];
const folders = Folders.get();
if (folders) {
for (const folder of folders) {
if (folder.locales) {
for (const locale of folder.locales) {
if (!i18nSettings.some((i18n) => i18n.locale === locale.locale)) {
i18nSettings.push(locale);
}
}
}
}
}
return i18nSettings;
}
/**
* Retrieves the I18nConfig settings from the application.
* @returns An array of I18nConfig objects if settings are found, otherwise undefined.
@@ -65,6 +89,25 @@ export class i18n {
return pageFolder.locales;
}
/**
* Checks if the locale is enabled for the given file path.
* @param filePath - The file path to check.
* @returns A promise that resolves to a boolean indicating whether the locale is enabled or not.
*/
public static async isLocaleEnabled(filePath: string): Promise<boolean> {
const i18nSettings = await i18n.getSettings(filePath);
if (!i18nSettings) {
return false;
}
const pageFolder = Folders.getPageFolderByFilePath(filePath);
if (!pageFolder || !pageFolder.locale) {
return false;
}
return i18nSettings.some((i18n) => i18n.locale === pageFolder.locale);
}
/**
* Checks if the given file path corresponds to the default language.
* @param filePath - The file path to check.
@@ -84,6 +127,10 @@ export class i18n {
const fileInfo = await i18n.getFileInfo(filePath);
if (pageFolder.path) {
if (pageFolder.locale) {
return pageFolder.locale === pageFolder.defaultLocale;
}
let pageFolderPath = parseWinPath(pageFolder.path);
if (!pageFolderPath.endsWith('/')) {
pageFolderPath += '/';
@@ -120,9 +167,10 @@ export class i18n {
if (
pageFolder.path &&
pageFolder.locale &&
parseWinPath(fileInfo.dir).toLowerCase() === parseWinPath(pageFolderPath).toLowerCase()
) {
return i18nSettings.find((i18n) => i18n.locale === pageFolder?.defaultLocale);
return i18nSettings.find((i18n) => i18n.locale === pageFolder?.locale);
}
}
@@ -172,9 +220,9 @@ export class i18n {
let pageFolder = Folders.getPageFolderByFilePath(filePath);
const fileInfo = await i18n.getFileInfo(filePath);
if (pageFolder && pageFolder.defaultLocale) {
if (pageFolder && pageFolder.defaultLocale && pageFolder.localeSourcePath) {
for (const i18n of i18nSettings) {
const translation = join(pageFolder.path, i18n.path || '', fileInfo.filename);
const translation = join(pageFolder.localeSourcePath, i18n.path || '', fileInfo.filename);
if (await existsAsync(translation)) {
translations[i18n.locale] = {
locale: i18n,
@@ -224,20 +272,40 @@ export class i18n {
fileUri = Uri.file(fileUri);
}
const pageFolder = Folders.getPageFolderByFilePath(fileUri.fsPath);
if (!pageFolder || !pageFolder.localeSourcePath) {
Notifications.error(l10n.t(LocalizationKey.commandsI18nCreateErrorNoContentFolder));
return;
}
const i18nSettings = await i18n.getSettings(fileUri.fsPath);
if (!i18nSettings) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
return;
}
const isDefaultLanguage = await i18n.isDefaultLanguage(fileUri.fsPath);
if (!isDefaultLanguage) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNotDefaultLocale));
const sourceLocale = await i18n.getLocale(fileUri.fsPath);
if (!sourceLocale || !sourceLocale.locale) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocaleDefinition));
return;
}
const translations = (await i18n.getTranslations(fileUri.fsPath)) || {};
const targetLocales = i18nSettings.filter((i18nSetting) => {
return (
i18nSetting.path &&
i18nSetting.locale !== sourceLocale.locale &&
!translations[i18nSetting.locale]
);
});
if (targetLocales.length === 0) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocales));
return;
}
const locale = await window.showQuickPick(
i18nSettings.filter((i18n) => i18n.path).map((i18n) => i18n.title || i18n.locale),
targetLocales.map((i18n) => i18n.title || i18n.locale),
{
title: l10n.t(LocalizationKey.commandsI18nCreateQuickPickTitle),
placeHolder: l10n.t(LocalizationKey.commandsI18nCreateQuickPickPlaceHolder),
@@ -249,10 +317,10 @@ export class i18n {
return;
}
const selectedI18n = i18nSettings.find(
const targetLocale = i18nSettings.find(
(i18n) => i18n.title === locale || i18n.locale === locale
);
if (!selectedI18n || !selectedI18n.path) {
if (!targetLocale || !targetLocale.path) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
return;
}
@@ -280,7 +348,7 @@ export class i18n {
pageBundleDir = join(parse(pageBundleDir).dir);
}
const i18nDir = join(dir, selectedI18n.path, pageBundleDir);
const i18nDir = join(pageFolder.localeSourcePath, targetLocale.path, pageBundleDir);
if (!(await existsAsync(i18nDir))) {
await workspace.fs.createDirectory(Uri.file(i18nDir));
@@ -290,7 +358,8 @@ export class i18n {
article,
fileUri.fsPath,
contentType,
selectedI18n,
sourceLocale,
targetLocale,
i18nDir
);
@@ -300,9 +369,8 @@ export class i18n {
return;
}
const sourceLocale = await i18n.getLocale(fileUri.fsPath);
if (sourceLocale?.locale) {
article = await i18n.translate(article, sourceLocale, selectedI18n);
article = await i18n.translate(article, sourceLocale, targetLocale);
}
const newFileUri = Uri.file(newFilePath);
@@ -318,7 +386,7 @@ export class i18n {
Notifications.info(
l10n.t(
LocalizationKey.commandsI18nCreateSuccessCreated,
selectedI18n.title || selectedI18n.locale
sourceLocale.title || sourceLocale.locale
)
);
}
@@ -336,12 +404,6 @@ export class i18n {
targetLocale: I18nConfig
) {
return new Promise<ParsedFrontMatter>(async (resolve) => {
const authKey = await Extension.getInstance().getSecret(ExtensionState.Secrets.DeeplApiKey);
if (!authKey) {
resolve(article);
return;
}
await window.withProgress(
{
location: ProgressLocation.Notification,
@@ -349,43 +411,26 @@ export class i18n {
cancellable: false
},
async () => {
const title = article.data.title;
const description = article.data.description;
const content = article.content;
try {
const body = JSON.stringify({
text: [title, description, content],
source_lang: sourceLocale.locale,
target_lang: targetLocale.locale
});
const title = article.data.title || '';
const description = article.data.description || '';
const content = article.content || '';
let host = authKey.endsWith(':fx') ? 'api-free.deepl.com' : 'api.deepl.com';
const text = [title, description, content];
const translations = await Translations.translate(
text,
sourceLocale.locale,
targetLocale.locale
);
const response = await fetch(`https://${host}/v2/translate`, {
method: 'POST',
headers: {
Authorization: `DeepL-Auth-Key ${authKey}`,
'User-Agent': `FrontMatterCMS/${Extension.getInstance().version}`,
'Content-Type': 'application/json',
'content-length': body.length.toString(),
Accept: 'application/json'
},
body
});
if (!response.ok) {
throw new Error(`DeepL: ${response.statusText}`);
if (!translations || translations.length < 3) {
resolve(article);
return;
}
const data = await response.json();
if (!data.translations || data.translations.length < 3) {
throw new Error('DeepL: Invalid response');
}
article.data.title = data.translations[0].text;
article.data.description = data.translations[1].text;
article.content = data.translations[2].text;
article.data.title = article.data.title ? translations[0] : '';
article.data.description = article.data.description ? translations[1] : '';
article.content = article.content ? translations[2] : '';
} catch (error) {
Notifications.error(`${(error as Error).message}`);
}
@@ -460,7 +505,8 @@ export class i18n {
* @param article - The parsed front matter of the article.
* @param filePath - The path of the file containing the front matter.
* @param contentType - The content type of the article.
* @param i18nConfig - The configuration for internationalization.
* @param sourceLocale - The source locale.
* @param targetLocale - The target locale.
* @param i18nDir - The directory where the i18n files are located.
* @returns A Promise that resolves to the updated parsed front matter.
*/
@@ -468,7 +514,8 @@ export class i18n {
article: ParsedFrontMatter,
filePath: string,
contentType: IContentType,
i18nConfig: I18nConfig,
sourceLocale: I18nConfig,
targetLocale: I18nConfig,
i18nDir: string
): Promise<ParsedFrontMatter> {
const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image');

View File

@@ -33,6 +33,12 @@ export const ExtensionState = {
},
Secrets: {
DeeplApiKey: `frontMatter:Secrets:DeeplApiKey`
Deepl: {
ApiKey: `frontMatter:Secrets:DeeplApiKey`
},
Azure: {
TranslatorKey: `frontMatter:Secrets:AzureTranslatorKey`,
TranslatorRegion: `frontMatter:Secrets:AzureTranslatorRegion`
}
}
};

View File

@@ -21,6 +21,9 @@ export const GeneralCommands = {
get: 'getSecret',
set: 'setSecret'
},
content: {
locales: 'getContentLocales'
},
runCommand: 'runCommand',
getLocalization: 'getLocalization',
openOnWebsite: 'openOnWebsite'

View File

@@ -10,3 +10,16 @@ export const SENTRY_LINK =
'https://1ac45704bbe74264a7b4674bdc2abf48@o1022172.ingest.sentry.io/5988293';
export const DOCS_SUBMODULES = 'https://frontmatter.codes/docs/git-integration#git-submodules';
export const WEBSITE_LINKS = {
root: 'https://frontmatter.codes',
api: {
metrics: 'https://frontmatter.codes/api/metrics',
ai: 'https://frontmatter.codes/api/ai'
},
docs: {
dataDashboard: 'https://frontmatter.codes/docs/dashboard/datafiles-view',
snippets: `https://frontmatter.codes/docs/snippets`,
snippetsPlaceholders: `https://frontmatter.codes/docs/snippets#placeholders`
}
};

View File

@@ -1,5 +1,6 @@
export const SentryIgnore = [
`ResizeObserver loop limit exceeded`,
`Cannot read properties of undefined (reading 'unobserve')`,
`TypeError: Cannot read properties of undefined (reading 'unobserve')`
`TypeError: Cannot read properties of undefined (reading 'unobserve')`,
`ResizeObserver loop completed with undelivered notifications.`
];

View File

@@ -8,7 +8,7 @@ export const CONTEXT = {
isValidFile: 'frontMatter:file:isValid',
isDevelopment: 'frontMatter:isDevelopment',
isI18nDefault: 'frontMatter:i18n:default',
isI18nEnabled: 'frontMatter:i18n:enabled',
hasViewModes: 'frontMatter:has:modes',

View File

@@ -133,13 +133,3 @@ export const SETTING_CONTENT_FOLDERS = 'content.folders';
* Use the `isPublishDate` property on the content type datetime field instead
*/
export const SETTING_DATE_FIELD = 'taxonomy.dateField';
/**
* @deprecated
* Use the `isModifiedDate` property on the content type datetime field instead
*/
export const SETTING_MODIFIED_FIELD = 'taxonomy.modifiedField';
/**
* @deprecated
* Use the `frontMatter.content.snippets` setting instead
*/
export const SETTING_DASHBOARD_MEDIA_SNIPPET = 'dashboard.mediaSnippet';

View File

@@ -30,7 +30,7 @@ export interface IAppProps {
export const App: React.FunctionComponent<IAppProps> = ({
showWelcome
}: React.PropsWithChildren<IAppProps>) => {
const { pages, settings, localeReady } = useMessages();
const { pages, settings } = useMessages();
const view = useRecoilValue(DashboardViewSelector);
const mode = useRecoilValue(ModeAtom);
const [isDevMode, setIsDevMode] = useState(false);
@@ -70,7 +70,7 @@ export const App: React.FunctionComponent<IAppProps> = ({
}
}, []);
if (!settings || !localeReady) {
if (!settings) {
return <Spinner />;
}

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import useSelectedItems from '../../hooks/useSelectedItems';
import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react';
import { useMemo } from 'react';
export interface IItemSelectionProps {
filePath: string;
isRowItem?: boolean;
}
export const ItemSelection: React.FunctionComponent<IItemSelectionProps> = ({
filePath,
isRowItem
}: React.PropsWithChildren<IItemSelectionProps>) => {
const { onMultiSelect, selectedFiles } = useSelectedItems();
const cssNames = useMemo(() => {
if (isRowItem) {
return 'block';
}
return `${selectedFiles.includes(filePath) ? 'block' : 'hidden'} absolute top-2 left-2`;
}, [isRowItem, selectedFiles]);
return (
<div className={`${cssNames} group-hover:block`}>
<VSCodeCheckbox
style={{
boxShadow: isRowItem ? "" : "0 0 3px var(--frontmatter-border-preserve)"
}}
onClick={(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
onMultiSelect(filePath);
}}
checked={selectedFiles.includes(filePath)} />
</div>
);
};

View File

@@ -230,7 +230,7 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
}
{
locale && isDefaultLocale && (
locale && (
<DropdownMenuItem onClick={() => runCommand(COMMAND_NAME.i18n.create)}>
<LanguageIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsCreate)}</span>

View File

@@ -11,6 +11,7 @@ import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { TelemetryEvent } from '../../../constants';
import { PageLayout } from '../Layout/PageLayout';
import { FilesProvider } from '../../providers/FilesProvider';
export interface IContentsProps {
pages: Page[];
@@ -32,18 +33,20 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({
}, []);
return (
<PageLayout folders={pageFolders} totalPages={pageItems.length}>
<div className="w-full flex-grow max-w-full mx-auto pb-6">
{loading ? <Spinner type={loading} /> : <Overview pages={pageItems} settings={settings} />}
</div>
<FilesProvider files={pageItems}>
<PageLayout folders={pageFolders} totalPages={pageItems.length}>
<div className="w-full flex-grow max-w-full mx-auto pb-6">
{loading ? <Spinner type={loading} /> : <Overview pages={pageItems} settings={settings} />}
</div>
<SponsorMsg
beta={settings?.beta}
version={settings?.versionInfo}
isBacker={settings?.isBacker}
/>
<SponsorMsg
beta={settings?.beta}
version={settings?.versionInfo}
isBacker={settings?.isBacker}
/>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=content" alt="Content metrics" />
</PageLayout>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=content" alt="Content metrics" />
</PageLayout>
</FilesProvider>
);
};

View File

@@ -17,6 +17,7 @@ import { useNavigate } from 'react-router-dom';
import { routePaths } from '../..';
import useCard from '../../hooks/useCard';
import { I18nLabel } from './I18nLabel';
import { ItemSelection } from '../Common/ItemSelection';
export interface IItemProps extends Page { }
@@ -133,6 +134,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({
}
</button>
<ItemSelection filePath={pageData.fmFilePath} />
<div className="relative p-4 w-full grow">
{
(statusPlaceholder || datePlaceholder) && (
@@ -232,6 +235,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({
className={`px-5 cursor-pointer w-full text-left grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8 py-2 border-b hover:bg-opacity-70 border-[var(--frontmatter-border)] hover:bg-[var(--vscode-sideBar-background)]`}
>
<div className="col-span-8 font-bold truncate flex items-center space-x-4">
<ItemSelection filePath={pageData.fmFilePath} isRowItem />
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={openFile}>

View File

@@ -7,6 +7,7 @@ import { messageHandler } from '@estruyf/vscode/dist/client';
import useCard from '../../hooks/useCard';
import { SettingsSelector } from '../../state';
import { useRecoilValue } from 'recoil';
import { ItemSelection } from '../Common/ItemSelection';
export interface IPinnedItemProps extends Page { }
@@ -21,7 +22,7 @@ export const PinnedItem: React.FunctionComponent<IPinnedItemProps> = ({
}, [pageData.fmFilePath]);
return (
<li className='group flex w-full border border-[var(--frontmatter-border)] rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-sideBarTitle-foreground)]'>
<li className='group flex w-full border border-[var(--frontmatter-border)] rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-sideBarTitle-foreground)] relative'>
<button onClick={openFile} className='relative h-full w-1/3'>
{
pageData["fmPreviewImage"] ? (
@@ -41,6 +42,8 @@ export const PinnedItem: React.FunctionComponent<IPinnedItemProps> = ({
}
</button>
<ItemSelection filePath={pageData.fmFilePath} />
<button onClick={openFile} className='relative w-2/3 p-4 pr-6 text-left flex items-start'>
<p className='font-bold'>{escapedTitle}</p>

View File

@@ -17,7 +17,7 @@ import { Container } from './SortableContainer';
import { SortableItem } from './SortableItem';
import { ChevronRightIcon, CircleStackIcon } from '@heroicons/react/24/outline';
import { DataType } from '../../../models/DataType';
import { TelemetryEvent } from '../../../constants';
import { TelemetryEvent, WEBSITE_LINKS } from '../../../constants';
import { NavigationItem } from '../Layout';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
@@ -265,7 +265,7 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
<p className="text-xl mt-4">
<a
className={`text-[var(--frontmatter-link)] hover:text-[var(--frontmatter-link-hover)]`}
href={`https://frontmatter.codes/docs/dashboard#data-files-view`}
href={WEBSITE_LINKS.docs.dataDashboard}
title={l10n.t(LocalizationKey.dashboardDataViewDataViewGetStartedLink)}
>
{l10n.t(LocalizationKey.dashboardDataViewDataViewGetStartedLink)}

View File

@@ -0,0 +1,255 @@
import * as React from 'react';
import { NavigationType, Page } from '../../models';
import { CommandLineIcon, PencilIcon, TrashIcon, ChevronDownIcon, XMarkIcon, EyeIcon, LanguageIcon } from '@heroicons/react/24/outline';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MultiSelectedItemsAtom, SelectedItemActionAtom, SelectedMediaFolderSelector, SettingsSelector } from '../../state';
import { ActionsBarItem } from './ActionsBarItem';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { Alert } from '../Modals/Alert';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { CustomScript, ScriptType } from '../../../models';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { useFilesContext } from '../../providers/FilesProvider';
import { COMMAND_NAME, GeneralCommands } from '../../../constants';
export interface IActionsBarProps {
view: NavigationType;
}
export const ActionsBar: React.FunctionComponent<IActionsBarProps> = ({
view
}: React.PropsWithChildren<IActionsBarProps>) => {
const [selectedFiles, setSelectedFiles] = useRecoilState(MultiSelectedItemsAtom);
const [, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
const [showAlert, setShowAlert] = React.useState(false);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const settings = useRecoilValue(SettingsSelector);
const { files } = useFilesContext();
const viewFile = React.useCallback(() => {
if (selectedFiles.length === 1) {
if (view === NavigationType.Contents) {
messageHandler.send(DashboardMessage.openFile, selectedFiles[0]);
} else if (view === NavigationType.Media) {
setSelectedItemAction({ path: selectedFiles[0], action: 'view' })
}
}
}, [selectedFiles]);
const onDeleteConfirm = React.useCallback(() => {
for (const file of selectedFiles) {
if (file) {
if (view === NavigationType.Contents) {
messageHandler.send(DashboardMessage.deleteFile, file);
} else if (view === NavigationType.Media) {
messageHandler.send(DashboardMessage.deleteMedia, {
file: file,
folder: selectedFolder
});
}
}
}
setSelectedFiles([]);
setShowAlert(false);
}, [selectedFiles]);
const runCustomScript = React.useCallback((script: CustomScript) => {
for (const file of selectedFiles) {
messageHandler.send(DashboardMessage.runCustomScript, {
script,
path: file
});
}
}, [selectedFiles]);
const languageActions = React.useMemo(() => {
const actions: React.ReactNode[] = [];
if (view === NavigationType.Contents && files.length > 0 && selectedFiles.length === 1) {
const selectedItem = selectedFiles[0];
const page = ((files || []) as Page[]).find((f: Page) => f.fmFilePath === selectedItem);
if (page?.fmLocale) {
const locale = page.fmLocale;
const translations = page.fmTranslations;
actions.push(
<ActionsBarItem
key="translate"
onClick={() => {
messageHandler.send(GeneralCommands.toVSCode.runCommand, {
command: COMMAND_NAME.i18n.create,
args: selectedItem
})
}}>
<LanguageIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonTranslate)}</span>
</ActionsBarItem>
)
if (translations && Object.keys(translations).length > 0) {
const crntLocale = translations[locale.locale];
const otherLocales = Object.entries(translations).filter(([key]) => key !== locale.locale);
if (otherLocales.length > 0) {
actions.push(
<DropdownMenu>
<DropdownMenuTrigger
className='flex items-center text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]'
>
<LanguageIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonLanguages)}</span>
<ChevronDownIcon className="ml-2 h-4 w-4" aria-hidden={true} />
</DropdownMenuTrigger>
<DropdownMenuContent align='start'>
<DropdownMenuItem onClick={() => messageHandler.send(DashboardMessage.openFile, crntLocale.path)}>
<span>{crntLocale.locale.title || crntLocale.locale.locale}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
{
otherLocales.map(([key, value]) => (
<DropdownMenuItem
key={key}
onClick={() => messageHandler.send(DashboardMessage.openFile, value.path)}
>
<span>{value.locale.title || value.locale.locale}</span>
</DropdownMenuItem>
))
}
</DropdownMenuContent>
</DropdownMenu>
)
}
}
}
}
return actions;
}, [files, selectedFiles]);
const customScriptActions = React.useMemo(() => {
if (!settings?.scripts) {
return null;
}
const { scripts } = settings;
let crntScripts: CustomScript[] = [];
if (view === NavigationType.Contents) {
crntScripts = (scripts || [])
.filter((script) => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk && !script.hidden);
} else if (view === NavigationType.Media) {
crntScripts = (scripts || [])
.filter((script) => script.type === ScriptType.MediaFile && !script.hidden);
}
if (crntScripts.length > 0) {
return (
<DropdownMenu>
<DropdownMenuTrigger
className='flex items-center text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)] disabled:opacity-50 disabled:hover:text-[var(--vscode-tab-inactiveForeground)]'
disabled={selectedFiles.length === 0}
>
<CommandLineIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonScripts)}</span>
<ChevronDownIcon className="ml-2 h-4 w-4" aria-hidden={true} />
</DropdownMenuTrigger>
<DropdownMenuContent align='start'>
{
crntScripts.map((script) => (
<DropdownMenuItem
key={script.id || script.title}
onClick={() => runCustomScript(script)}
>
<CommandLineIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{script.title}</span>
</DropdownMenuItem>
))
}
</DropdownMenuContent>
</DropdownMenu>
);
}
return null;
}, [view, settings?.scripts, selectedFiles]);
return (
<>
<div
className={`w-full flex items-center justify-between py-2 px-4 border-b bg-[var(--vscode-sideBar-background)] text-[var(--vscode-sideBar-foreground)] border-[var(--frontmatter-border)]`}
aria-label="Item actions"
>
<div className='flex items-center space-x-6'>
<ActionsBarItem
disabled={selectedFiles.length === 0 || selectedFiles.length > 1}
onClick={viewFile}
>
<EyeIcon className="w-4 h-4 mr-2" aria-hidden="true" />
<span>{l10n.t(LocalizationKey.commonView)}</span>
</ActionsBarItem>
{
view === NavigationType.Media && (
<>
<ActionsBarItem
disabled={selectedFiles.length === 0 || selectedFiles.length > 1}
onClick={() => setSelectedItemAction({
path: selectedFiles[0],
action: 'edit'
})}
>
<PencilIcon className="w-4 h-4 mr-2" aria-hidden="true" />
<span>{l10n.t(LocalizationKey.commonEdit)}</span>
</ActionsBarItem>
</>
)
}
{languageActions}
{customScriptActions}
<ActionsBarItem
className='hover:text-[var(--vscode-statusBarItem-errorBackground)]'
disabled={selectedFiles.length === 0}
onClick={() => setShowAlert(true)}
>
<TrashIcon className="w-4 h-4 mr-2" aria-hidden="true" />
<span>{l10n.t(LocalizationKey.commonDelete)}</span>
</ActionsBarItem>
</div>
{
selectedFiles.length > 0 && (
<button
type="button"
className='flex items-center hover:text-[var(--vscode-statusBarItem-warningBackground)]'
onClick={() => setSelectedFiles([])}
>
<XMarkIcon className="w-4 h-4 mr-1" aria-hidden="true" />
<span>{l10n.t(LocalizationKey.dashboardHeaderActionsBarItemsSelected)}</span>
</button>
)
}
</div>
{showAlert && (
<Alert
title={`${l10n.t(LocalizationKey.dashboardHeaderActionsBarAlertDeleteTitle)}`}
description={l10n.t(LocalizationKey.dashboardHeaderActionsBarAlertDeleteDescription)}
okBtnText={l10n.t(LocalizationKey.commonDelete)}
cancelBtnText={l10n.t(LocalizationKey.commonCancel)}
dismiss={() => setShowAlert(false)}
trigger={onDeleteConfirm}
/>
)}
</>
);
};

View File

@@ -0,0 +1,26 @@
import * as React from 'react';
import { cn } from '../../../utils/cn';
export interface IActionsBarItemProps {
className?: string;
disabled?: boolean;
onClick?: () => void;
}
export const ActionsBarItem: React.FunctionComponent<IActionsBarItemProps> = ({
children,
className,
disabled,
onClick
}: React.PropsWithChildren<IActionsBarItemProps>) => {
return (
<button
type="button"
className={cn(`flex items-center text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)] disabled:opacity-50 disabled:hover:text-[var(--vscode-tab-inactiveForeground)]`, className)}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};

View File

@@ -4,24 +4,25 @@ import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { HOME_PAGE_NAVIGATION_ID } from '../../../constants';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { SearchAtom, SelectedMediaFolderAtom, SettingsAtom } from '../../state';
import { SearchAtom, SettingsAtom } from '../../state';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import useMediaFolder from '../../hooks/useMediaFolder';
export interface IBreadcrumbProps { }
export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (
_: React.PropsWithChildren<IBreadcrumbProps>
) => {
const [selectedFolder, setSelectedFolder] = useRecoilState(SelectedMediaFolderAtom);
const { selectedFolder, updateFolder } = useMediaFolder();
const [, setSearchValue] = useRecoilState(SearchAtom);
const [folders, setFolders] = React.useState<string[]>([]);
const settings = useRecoilValue(SettingsAtom);
const updateFolder = (folder: string) => {
const updateMediaFolder = React.useCallback((folder: string) => {
setSearchValue('');
setSelectedFolder(folder);
};
updateFolder(folder);
}, [updateFolder, setSearchValue]);
React.useEffect(() => {
if (!settings) {
@@ -79,11 +80,11 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (
}, [selectedFolder, settings]);
return (
<ol role="list" className="flex space-x-4 px-5 flex-1">
<ol role="list" className="flex space-x-2 px-4 flex-1">
<li className="flex">
<div className="flex items-center">
<button
onClick={() => setSelectedFolder(HOME_PAGE_NAVIGATION_ID)}
onClick={() => updateMediaFolder(HOME_PAGE_NAVIGATION_ID)}
className={`text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]`}
>
<HomeIcon className="flex-shrink-0 h-5 w-5" aria-hidden="true" />
@@ -106,8 +107,8 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (
</svg>
<button
onClick={() => updateFolder(folder)}
className={`ml-4 text-sm font-medium text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]`}
onClick={() => updateMediaFolder(folder)}
className={`ml-2 text-sm font-medium text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]`}
>
{basename(folder)}
</button>

View File

@@ -18,7 +18,7 @@ export const Filters: React.FunctionComponent<IFiltersProps> = (_: React.PropsWi
const settings = useRecoilValue(SettingsSelector);
const location = useLocation();
const otherFilters = useMemo(() => settings?.filters?.filter((filter) => filter !== "pageFolders" && filter !== "tags" && filter !== "categories"), [settings?.filters]);
const otherFilters = useMemo(() => settings?.filters?.filter((filter) => filter !== "contentFolders" && filter !== "tags" && filter !== "categories"), [settings?.filters]);
const otherFilterValues = useMemo(() => {
return otherFilters?.map((filter) => {
@@ -77,7 +77,7 @@ export const Filters: React.FunctionComponent<IFiltersProps> = (_: React.PropsWi
<LanguageFilter />
{
settings?.filters?.includes("pageFolders") && (
settings?.filters?.includes("contentFolders") && (
<FoldersFilter />
)
}

View File

@@ -4,7 +4,7 @@ import { FolderAtom, SettingsSelector } from '../../state';
import { MenuButton, MenuItem } from '../Menu';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
export interface IFoldersFilterProps { }
@@ -14,7 +14,11 @@ export const FoldersFilter: React.FunctionComponent<
const DEFAULT_TYPE = l10n.t(LocalizationKey.dashboardHeaderFoldersDefault);
const [crntFolder, setCrntFolder] = useRecoilState(FolderAtom);
const settings = useRecoilValue(SettingsSelector);
const contentFolders = settings?.contentFolders || [];
const contentFolders = React.useMemo(() => {
return settings?.contentFolders
.filter((folder, index, self) => index === self.findIndex((t) => t.originalPath === folder.originalPath)) || [];
}, [settings?.contentFolders]);
if (contentFolders.length <= 1) {
return null;

View File

@@ -6,7 +6,7 @@ import { DashboardMessage } from '../../DashboardMessage';
import { Grouping } from '.';
import { ViewSwitch } from './ViewSwitch';
import { useRecoilValue, useResetRecoilState } from 'recoil';
import { GroupingSelector, SortingAtom } from '../../state';
import { GroupingSelector, MultiSelectedItemsAtom, SortingAtom } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { ClearFilters } from './ClearFilters';
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
@@ -18,8 +18,7 @@ import { ArrowTopRightOnSquareIcon, BoltIcon, PlusIcon } from '@heroicons/react/
import { HeartIcon } from '@heroicons/react/24/solid';
import { useLocation, useNavigate } from 'react-router-dom';
import { routePaths } from '../..';
import { useEffect, useMemo } from 'react';
import { SyncButton } from './SyncButton';
import { useMemo } from 'react';
import { Pagination } from './Pagination';
import { GroupOption } from '../../constants/GroupOption';
import usePagination from '../../hooks/usePagination';
@@ -32,6 +31,7 @@ import { SettingsLink } from '../SettingsView/SettingsLink';
import { Link } from '../Common/Link';
import { SPONSOR_LINK } from '../../../constants';
import { Filters } from './Filters';
import { ActionsBar } from './ActionsBar';
export interface IHeaderProps {
header?: React.ReactNode;
@@ -51,6 +51,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
}: React.PropsWithChildren<IHeaderProps>) => {
const grouping = useRecoilValue(GroupingSelector);
const resetSorting = useResetRecoilState(SortingAtom);
const resetSelectedItems = useResetRecoilState(MultiSelectedItemsAtom);
const location = useLocation();
const navigate = useNavigate();
const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination);
@@ -70,6 +71,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
const updateView = (view: NavigationType) => {
navigate(routePaths[view]);
resetSorting();
resetSelectedItems();
};
const runBulkScript = (script: CustomScript) => {
@@ -122,7 +124,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
return (
<div className={`w-full sticky top-0 z-20 bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)]`}>
<div className={`mb-0 border-b flex justify-between bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)] border-[var(--frontmatter-border)]`}>
<div className={`overflow-x-auto mb-0 border-b flex justify-between bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)] border-[var(--frontmatter-border)]`}>
<Tabs onNavigate={updateView} />
<div className='flex items-center space-x-2 pr-4'>
@@ -160,12 +162,8 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
{location.pathname === routePaths.contents && (
<>
<div className={`px-4 mt-3 mb-2 flex items-center justify-between`}>
<Searchbox />
<div className={`flex items-center justify-end space-x-4 flex-1`}>
{/* <SyncButton /> */}
<div className={`px-4 mt-2 mb-2 flex items-center justify-between`}>
<div className={`flex items-center justify-start space-x-4 flex-1`}>
<ChoiceButton
title={l10n.t(LocalizationKey.dashboardHeaderHeaderCreateContent)}
choices={choiceOptions}
@@ -173,6 +171,8 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
disabled={!settings?.initialized}
/>
</div>
<Searchbox />
</div>
<div className={`px-4 flex flex-row items-center border-b justify-between border-[var(--frontmatter-border)]`}>
@@ -186,7 +186,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
</div>
<div
className={`py-4 px-5 w-full flex items-center justify-between lg:justify-end border-b space-x-4 lg:space-x-6 xl:space-x-8 bg-[var(--vscode-panel-background)] border-[var(--frontmatter-border)]`}
className={`overflow-x-auto py-2 px-4 w-full flex items-center justify-between lg:justify-end border-b space-x-4 lg:space-x-6 xl:space-x-8 bg-[var(--vscode-panel-background)] border-[var(--frontmatter-border)]`}
>
<ClearFilters />
@@ -208,6 +208,8 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
<Pagination totalPages={totalPages || 0} />
</div>
)}
<ActionsBar view={NavigationType.Contents} />
</>
)}
@@ -216,6 +218,8 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
<MediaHeaderTop />
<MediaHeaderBottom />
<ActionsBar view={NavigationType.Media} />
</>
)}

View File

@@ -40,7 +40,7 @@ export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({
}, [debounceSearch]);
return (
<div className="flex space-x-4 flex-1">
<div className="flex justify-end space-x-4 flex-1">
<div className="min-w-0">
<label htmlFor="search" className="sr-only">
{l10n.t(LocalizationKey.commonSearch)}

View File

@@ -20,7 +20,7 @@ export const PageLayout: React.FunctionComponent<IPageLayoutProps> = ({
const settings = useRecoilValue(SettingsSelector);
return (
<div className="flex flex-col h-full overflow-auto">
<div className="flex flex-col h-full overflow-y-auto overflow-x-hidden">
<Header header={header} folders={folders} totalPages={totalPages} settings={settings} />
<div

View File

@@ -128,7 +128,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
</div>
<div className="relative mt-6 flex-1 px-4 sm:px-6">
<div className="absolute inset-0 px-4 sm:px-6 space-y-8">
<div className="space-y-8">
<div>
{(isImageFile || isVideoFile) && (
<div className={`block w-full aspect-w-10 aspect-h-7 overflow-hidden border rounded border-[var(--frontmatter-border)] bg-[var(--vscode-editor-background)]`}>

View File

@@ -5,7 +5,6 @@ import { DashboardMessage } from '../../DashboardMessage';
import {
AllContentFoldersAtom,
AllStaticFoldersAtom,
SelectedMediaFolderAtom,
SettingsSelector,
ViewDataSelector
} from '../../state';
@@ -18,13 +17,14 @@ import { extname } from 'path';
import { parseWinPath } from '../../../helpers/parseWinPath';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import useMediaFolder from '../../hooks/useMediaFolder';
export interface IFolderCreationProps { }
export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
props: React.PropsWithChildren<IFolderCreationProps>
_: React.PropsWithChildren<IFolderCreationProps>
) => {
const selectedFolder = useRecoilValue(SelectedMediaFolderAtom);
const { selectedFolder } = useMediaFolder();
const settings = useRecoilValue(SettingsSelector);
const allStaticFolders = useRecoilValue(AllStaticFoldersAtom);
const allContentFolders = useRecoilValue(AllContentFoldersAtom);
@@ -90,7 +90,7 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
if (scripts.length > 0) {
return (
<div className="flex flex-1 justify-end">
<div className="flex flex-1 justify-start">
{renderPostAssetsButton}
<ChoiceButton
title={l10n.t(LocalizationKey.dashboardMediaFolderCreationFolderCreate)}
@@ -107,7 +107,7 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
}
return (
<div className="flex flex-1 justify-end">
<div className="flex flex-1 justify-start">
{renderPostAssetsButton}
<button
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium focus:outline-none rounded text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50`}

View File

@@ -1,8 +1,9 @@
import { FolderIcon } from '@heroicons/react/24/solid';
import { basename, join } from 'path';
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { SelectedMediaFolderAtom } from '../../state';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import useMediaFolder from '../../hooks/useMediaFolder';
export interface IFolderItemProps {
folder: string;
@@ -15,7 +16,7 @@ export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({
wsFolder,
staticFolder
}: React.PropsWithChildren<IFolderItemProps>) => {
const [, setSelectedFolder] = useRecoilState(SelectedMediaFolderAtom);
const { updateFolder } = useMediaFolder();
const relFolderPath = wsFolder ? folder.replace(wsFolder, '') : folder;
@@ -29,9 +30,9 @@ export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({
className={`group relative hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-editor-foreground)] hover:text-[var(--vscode-list-activeSelectionForeground)]`}
>
<button
title={isContentFolder ? 'Content directory folder' : 'Public directory folder'}
title={isContentFolder ? l10n.t(LocalizationKey.dashboardMediaFolderItemContentDirectory) : l10n.t(LocalizationKey.dashboardMediaFolderItemPublicDirectory)}
className={`p-4 w-full flex flex-row items-center h-full`}
onClick={() => setSelectedFolder(folder)}
onClick={() => updateFolder(folder)}
>
<div className="relative mr-4">
<FolderIcon className={`h-12 w-12`} />

View File

@@ -7,7 +7,7 @@ import {
PlusIcon,
VideoCameraIcon,
} from '@heroicons/react/24/outline';
import { basename, dirname } from 'path';
import { basename } from 'path';
import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
@@ -17,19 +17,21 @@ import { MediaInfo } from '../../../models/MediaPaths';
import { DashboardMessage } from '../../DashboardMessage';
import {
LightboxAtom,
SelectedItemActionAtom,
SelectedMediaFolderSelector,
SettingsSelector,
ViewDataSelector
} from '../../state';
import { Alert } from '../Modals/Alert';
import { InfoDialog } from '../Modals/InfoDialog';
import { DetailsSlideOver } from './DetailsSlideOver';
import { MediaSnippetForm } from './MediaSnippetForm';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { ItemMenu } from './ItemMenu';
import { getRelPath } from '../../utils';
import { Snippet } from '../../../models';
import useMediaInfo from '../../hooks/useMediaInfo';
import { ItemSelection } from '../Common/ItemSelection';
export interface IItemProps {
media: MediaInfo;
@@ -39,17 +41,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({
media,
}: React.PropsWithChildren<IItemProps>) => {
const [, setLightbox] = useRecoilState(LightboxAtom);
const [, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
const [showAlert, setShowAlert] = useState(false);
const [showForm, setShowForm] = useState(false);
const [showSnippetSelection, setShowSnippetSelection] = useState(false);
const [snippet, setSnippet] = useState<Snippet | undefined>(undefined);
const [showDetails, setShowDetails] = useState(false);
const [showSnippetFormDialog, setShowSnippetFormDialog] = useState(false);
const [mediaData, setMediaData] = useState<any | undefined>(undefined);
const [filename, setFilename] = useState<string | null>(null);
const settings = useRecoilValue(SettingsSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const viewData = useRecoilValue(ViewDataSelector);
const { mediaFolder, mediaDetails, isAudio, isImage, isVideo } = useMediaInfo(media);
const relPath = useMemo(() => {
return getRelPath(media.fsPath, settings?.staticFolder, settings?.wsFolder);
@@ -74,19 +76,6 @@ export const Item: React.FunctionComponent<IItemProps> = ({
return viewData?.data?.position && mediaSnippets.length > 0;
}, [viewData, mediaSnippets]);
const getFolder = () => {
if (settings?.wsFolder && media.fsPath) {
let relPath = media.fsPath.split(settings.wsFolder).pop();
if (settings.staticFolder && relPath) {
relPath = relPath.split(settings.staticFolder).pop();
}
return dirname(parseWinPath(relPath) || '');
}
return '';
};
const getFileName = () => {
return basename(parseWinPath(media.fsPath) || '');
};
@@ -190,75 +179,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({
});
};
const getDimensions = () => {
if (media.dimensions) {
return `${media.dimensions.width} x ${media.dimensions.height}`;
}
return '';
};
const getSize = () => {
if (media?.size) {
const size = media.size / (1024 * 1024);
if (size > 1) {
return `${size.toFixed(2)} MB`;
} else {
return `${(size * 1024).toFixed(2)} KB`;
}
}
return '';
};
const getMediaDetails = () => {
let sizeDetails = [];
const dimensions = getDimensions();
if (dimensions) {
sizeDetails.push(dimensions);
}
const size = getSize();
if (size) {
sizeDetails.push(size);
}
return sizeDetails.join(' - ');
};
const openLightbox = useCallback(() => {
if (isImageFile) {
if (isImage) {
setLightbox(media.vsPath || '');
}
}, [media.vsPath]);
const updateMetadata = () => {
setShowForm(true);
setShowDetails(true);
};
const isVideoFile = useMemo(() => {
if (media.mimeType?.startsWith('video/')) {
return true;
}
return false;
}, [media]);
const isAudioFile = useMemo(() => {
if (media.mimeType?.startsWith('audio/')) {
return true;
}
return false;
}, [media]);
const isImageFile = useMemo(() => {
if (
media.mimeType?.startsWith('image/') &&
!media.mimeType?.startsWith('image/vnd.adobe.photoshop')
) {
return true;
}
return false;
const updateMetadata = useCallback(() => {
setSelectedItemAction({
path: media.fsPath,
action: 'edit'
});
}, [media]);
const renderMediaIcon = useMemo(() => {
@@ -273,15 +204,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({
return null;
}
if (isImageFile) {
if (isImage) {
return <PhotoIcon className={`h-1/2 ${colors}`} />;
}
if (isVideoFile) {
if (isVideo) {
icon = <VideoCameraIcon className={`h-4/6 ${colors}`} />;
}
if (isAudioFile) {
if (isAudio) {
icon = <MusicalNoteIcon className={`h-4/6 ${colors}`} />;
}
@@ -293,18 +224,18 @@ export const Item: React.FunctionComponent<IItemProps> = ({
</span>
</div>
);
}, [media, isImageFile, isVideoFile, isAudioFile]);
}, [media, isImage, isVideo, isAudio]);
const renderMedia = useMemo(() => {
if (isAudioFile) {
if (isAudio) {
return null;
}
if (isVideoFile) {
if (isVideo) {
return <video src={media.vsPath} className="mx-auto object-cover" controls muted />;
}
if (isImageFile) {
if (isImage) {
return (
<img src={media.vsPath} alt={basename(media.fsPath)} className="mx-auto object-cover" />
);
@@ -336,7 +267,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
<>
<li className={`group relative shadow-md hover:shadow-xl dark:shadow-none border rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-sideBarTitle-foreground)] border-[var(--frontmatter-border)]`}>
<button
className={`group/button relative block w-full aspect-w-10 aspect-h-7 overflow-hidden h-48 ${isImageFile ? 'cursor-pointer' : 'cursor-default'} border-b border-[var(--frontmatter-border)]`}
className={`group/button relative block w-full aspect-w-10 aspect-h-7 overflow-hidden h-48 ${isImage ? 'cursor-pointer' : 'cursor-default'} border-b border-[var(--frontmatter-border)]`}
onClick={hasViewData ? undefined : openLightbox}
>
<div
@@ -349,6 +280,9 @@ export const Item: React.FunctionComponent<IItemProps> = ({
>
{renderMedia}
</div>
<ItemSelection filePath={media.fsPath} />
{hasViewData && (
<div
className={`hidden group-hover/button:flex absolute top-0 right-0 bottom-0 left-0 items-center justify-center bg-black bg-opacity-70`}
@@ -379,6 +313,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({
</button>
</div>
)}
<ItemSelection filePath={media.fsPath} />
</div>
)}
</button>
@@ -393,14 +329,14 @@ export const Item: React.FunctionComponent<IItemProps> = ({
insertIntoArticle={insertIntoArticle}
insertSnippet={insertSnippet}
showUpdateMedia={updateMetadata}
showMediaDetails={() => setShowDetails(true)}
showMediaDetails={() => setSelectedItemAction({ path: media.fsPath, action: 'view' })}
processSnippet={processSnippet}
onDelete={() => setShowAlert(true)} />
<p className={`text-sm font-bold pointer-events-none flex items-center break-all text-[var(--vscode-foreground)]}`}>
{basename(parseWinPath(media.fsPath) || '')}
</p>
{!isImageFile && media.metadata.title && (
{!isImage && media.metadata.title && (
<p className={`mt-2 text-xs font-medium pointer-events-none flex flex-col items-start`}>
<b className={`mr-2`}>
{l10n.t(LocalizationKey.dashboardMediaCommonTitle)}:
@@ -430,7 +366,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
{l10n.t(LocalizationKey.dashboardMediaCommonSize)}:
</b>
<span className={`block mt-1 text-xs text-[var(--vscode-foreground)]`}>
{getMediaDetails()}
{mediaDetails}
</span>
</p>
)}
@@ -459,29 +395,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({
</InfoDialog>
)}
{showDetails && (
<DetailsSlideOver
imgSrc={media.vsPath || ''}
size={getSize()}
dimensions={getDimensions()}
folder={getFolder()}
media={media}
showForm={showForm}
isImageFile={isImageFile}
isVideoFile={isVideoFile}
onEdit={() => setShowForm(true)}
onEditClose={() => setShowForm(false)}
onDismiss={() => {
setShowDetails(false);
setShowForm(false);
}}
/>
)}
{showAlert && (
<Alert
title={`${l10n.t(LocalizationKey.commonDelete)}: ${basename(parseWinPath(media.fsPath) || '')}`}
description={l10n.t(LocalizationKey.dashboardMediaItemAlertDeleteDescription, getFolder())}
description={l10n.t(LocalizationKey.dashboardMediaItemAlertDeleteDescription, mediaFolder)}
okBtnText={l10n.t(LocalizationKey.commonDelete)}
cancelBtnText={l10n.t(LocalizationKey.commonCancel)}
dismiss={() => setShowAlert(false)}

View File

@@ -27,6 +27,8 @@ import { basename, extname, join } from 'path';
import { MediaInfo } from '../../../models';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { MediaItemPanel } from './MediaItemPanel';
import { FilesProvider } from '../../providers/FilesProvider';
export interface IMediaProps { }
@@ -162,111 +164,115 @@ export const Media: React.FunctionComponent<IMediaProps> = (
});
return (
<PageLayout>
<div className="w-full h-full pb-6" {...getRootProps()}>
{viewData?.data?.filePath && (
<div className={`text-lg text-center mb-6`}>
<p>{l10n.t(LocalizationKey.dashboardMediaMediaDescription)}</p>
<p className={`opacity-80 text-base`}>
{l10n.t(LocalizationKey.dashboardMediaMediaDragAndDrop)}
</p>
</div>
)}
{isDragActive && (
<div className={`absolute top-0 left-0 w-full h-full flex flex-col justify-center items-center z-50 text-[var(--vscode-foreground)] bg-[var(--vscode-editor-background)] opacity-75`}>
<ArrowUpTrayIcon className={`h-32`} />
<p className={`text-xl max-w-md text-center`}>
{selectedFolder
? l10n.t(LocalizationKey.dashboardMediaMediaFolderUpload, selectedFolder)
: l10n.t(LocalizationKey.dashboardMediaMediaFolderDefault, currentStaticFolder || 'public')}
</p>
</div>
)}
{allMedia.length === 0 && folders.length === 0 && !loading && (
<div className={`flex items-center justify-center h-full`}>
<div className={`max-w-xl text-center`}>
<FrontMatterIcon
className={`h-32 mx-auto opacity-90 mb-8 text-[var(--vscode-editor-foreground)]`}
/>
<p className={`text-xl font-medium`}>
{l10n.t(LocalizationKey.dashboardMediaMediaPlaceholder)}
<FilesProvider files={allMedia}>
<PageLayout>
<div className="w-full h-full pb-6" {...getRootProps()}>
{viewData?.data?.filePath && (
<div className={`text-lg text-center mb-6`}>
<p>{l10n.t(LocalizationKey.dashboardMediaMediaDescription)}</p>
<p className={`opacity-80 text-base`}>
{l10n.t(LocalizationKey.dashboardMediaMediaDragAndDrop)}
</p>
</div>
</div>
)}
{contentFolders &&
contentFolders.length > 0 &&
contentFolders.map(
(group, idx) =>
group.folders &&
group.folders.length > 0 && (
<div key={`group-${idx}`} className={`mb-8`}>
<h2 className="text-lg mb-8 first-letter:uppercase">
{l10n.t(LocalizationKey.dashboardMediaMediaContentFolder)}: <b>{group.title}</b>
</h2>
<List gap={0}>
{group.folders.map((folder) => (
<FolderItem
key={folder}
folder={folder}
staticFolder={currentStaticFolder}
wsFolder={settings?.wsFolder}
/>
))}
</List>
</div>
)
)}
{publicFolders && publicFolders.length > 0 && (
<div className={`mb-8`}>
{contentFolders && contentFolders.length > 0 && (
<h2 className="text-lg mb-8">
{l10n.t(LocalizationKey.dashboardMediaMediaPublicFolder)}
{currentStaticFolder && (
<span>
: <b>{currentStaticFolder}</b>
</span>
)}
</h2>
{isDragActive && (
<div className={`absolute top-0 left-0 w-full h-full flex flex-col justify-center items-center z-50 text-[var(--vscode-foreground)] bg-[var(--vscode-editor-background)] opacity-75`}>
<ArrowUpTrayIcon className={`h-32`} />
<p className={`text-xl max-w-md text-center`}>
{selectedFolder
? l10n.t(LocalizationKey.dashboardMediaMediaFolderUpload, selectedFolder)
: l10n.t(LocalizationKey.dashboardMediaMediaFolderDefault, currentStaticFolder || 'public')}
</p>
</div>
)}
{allMedia.length === 0 && folders.length === 0 && !loading && (
<div className={`flex items-center justify-center h-full`}>
<div className={`max-w-xl text-center`}>
<FrontMatterIcon
className={`h-32 mx-auto opacity-90 mb-8 text-[var(--vscode-editor-foreground)]`}
/>
<p className={`text-xl font-medium`}>
{l10n.t(LocalizationKey.dashboardMediaMediaPlaceholder)}
</p>
</div>
</div>
)}
{contentFolders &&
contentFolders.length > 0 &&
contentFolders.map(
(group, idx) =>
group.folders &&
group.folders.length > 0 && (
<div key={`group-${idx}`} className={`mb-8`}>
<h2 className="text-lg mb-8 first-letter:uppercase">
{l10n.t(LocalizationKey.dashboardMediaMediaContentFolder)}: <b>{group.title}</b>
</h2>
<List gap={0}>
{group.folders.map((folder) => (
<FolderItem
key={folder}
folder={folder}
staticFolder={currentStaticFolder}
wsFolder={settings?.wsFolder}
/>
))}
</List>
</div>
)
)}
<List gap={0}>
{publicFolders.map((folder) => (
<FolderItem
key={folder}
folder={folder}
staticFolder={currentStaticFolder}
wsFolder={settings?.wsFolder}
/>
))}
</List>
</div>
)}
{publicFolders && publicFolders.length > 0 && (
<div className={`mb-8`}>
{contentFolders && contentFolders.length > 0 && (
<h2 className="text-lg mb-8">
{l10n.t(LocalizationKey.dashboardMediaMediaPublicFolder)}
{currentStaticFolder && (
<span>
: <b>{currentStaticFolder}</b>
</span>
)}
</h2>
)}
<List>
{allMedia.map((file, idx) => (
<Item key={file.fsPath} media={file} />
))}
</List>
</div>
<List gap={0}>
{publicFolders.map((folder) => (
<FolderItem
key={folder}
folder={folder}
staticFolder={currentStaticFolder}
wsFolder={settings?.wsFolder}
/>
))}
</List>
</div>
)}
{loading && <Spinner />}
<List>
{allMedia.map((file, idx) => (
<Item key={file.fsPath} media={file} />
))}
</List>
</div>
<Lightbox />
<MediaItemPanel allMedia={allMedia} />
<SponsorMsg
beta={settings?.beta}
version={settings?.versionInfo}
isBacker={settings?.isBacker}
/>
{loading && <Spinner />}
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=media" alt="Media metrics" />
</PageLayout>
<Lightbox />
<SponsorMsg
beta={settings?.beta}
version={settings?.versionInfo}
isBacker={settings?.isBacker}
/>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=media" alt="Media metrics" />
</PageLayout>
</FilesProvider>
);
};

View File

@@ -81,14 +81,14 @@ export const MediaHeaderTop: React.FunctionComponent<
return (
<nav
className={`py-3 px-4 flex items-center justify-between border-b border-[var(--frontmatter-border)]`}
className={`py-2 px-4 flex items-center justify-between border-b border-[var(--frontmatter-border)]`}
aria-label="Pagination"
>
<Searchbox placeholder={l10n.t(LocalizationKey.dashboardMediaMediaHeaderTopSearchboxPlaceholder)} />
<FolderCreation />
<PaginationStatus />
<FolderCreation />
<Searchbox placeholder={l10n.t(LocalizationKey.dashboardMediaMediaHeaderTopSearchboxPlaceholder)} />
</nav>
);
};

View File

@@ -0,0 +1,60 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import { SelectedItemActionAtom } from '../../state';
import { MediaInfo } from '../../../models';
import { DetailsSlideOver } from './DetailsSlideOver';
import useMediaInfo from '../../hooks/useMediaInfo';
export interface IMediaItemPanelProps {
allMedia: MediaInfo[];
}
export const MediaItemPanel: React.FunctionComponent<IMediaItemPanelProps> = ({ allMedia }: React.PropsWithChildren<IMediaItemPanelProps>) => {
const [media, setMedia] = useState<MediaInfo | undefined>(undefined);
const [showForm, setShowForm] = useState(false);
const [showDetails, setShowDetails] = useState(false);
const [selectedItemAction, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
const { mediaFolder, mediaSize, mediaDimensions, isImage, isVideo } = useMediaInfo(media);
useEffect(() => {
if (selectedItemAction && selectedItemAction.path) {
const mediaFile = allMedia.find((m) => m.fsPath === selectedItemAction.path);
setMedia(mediaFile);
if (selectedItemAction.action === 'edit') {
setShowForm(true);
setShowDetails(true);
} else if (selectedItemAction.action === 'view') {
setShowForm(false);
setShowDetails(true);
}
setSelectedItemAction(undefined);
}
}, [allMedia, selectedItemAction])
if (showDetails && media) {
return (
<DetailsSlideOver
imgSrc={media.vsPath || ''}
size={mediaSize}
dimensions={mediaDimensions}
folder={mediaFolder}
media={media}
showForm={showForm}
isImageFile={isImage}
isVideoFile={isVideo}
onEdit={() => setShowForm(true)}
onEditClose={() => setShowForm(false)}
onDismiss={() => {
setShowDetails(false);
setShowForm(false);
setMedia(undefined);
}}
/>
);
}
return null;
};

View File

@@ -14,7 +14,7 @@ export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({
disabled
}: React.PropsWithChildren<IMenuButtonProps>) => {
return (
<div className={`group flex items-center ${disabled ? 'opacity-50' : ''}`}>
<div className={`group flex items-center shrink-0 ${disabled ? 'opacity-50' : ''}`}>
<div className={`mr-2 font-medium flex items-center text-[var(--vscode-tab-inactiveForeground)]`}>
{label}:
</div>

View File

@@ -10,26 +10,64 @@ export interface IIntegrationsViewProps { }
export const IntegrationsView: React.FunctionComponent<IIntegrationsViewProps> = ({ }: React.PropsWithChildren<IIntegrationsViewProps>) => {
const [deeplApiKey, setDeeplApiKey] = React.useState<string>('');
const [azureApiKey, setAzureApiKey] = React.useState<string>('');
const [azureRegion, setAzureRegion] = React.useState<string>('');
const [crntDeeplApiKey, setCrntDeeplApiKey] = React.useState<string>('');
const [crntAzureApiKey, setCrntAzureApiKey] = React.useState<string>('');
const [crntAzureRegion, setCrntAzureRegion] = React.useState<string>('');
const onSave = React.useCallback(() => {
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
key: ExtensionState.Secrets.DeeplApiKey,
value: crntDeeplApiKey
}).then((apiKey: string) => {
setDeeplApiKey(apiKey);
});
}, [crntDeeplApiKey]);
if (crntDeeplApiKey !== deeplApiKey) {
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
key: ExtensionState.Secrets.Deepl.ApiKey,
value: crntDeeplApiKey
}).then((apiKey: string) => {
setDeeplApiKey(apiKey);
});
}
const onChange = (_: string, value: string) => {
setCrntDeeplApiKey(value);
if (crntAzureApiKey !== azureApiKey) {
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
key: ExtensionState.Secrets.Azure.TranslatorKey,
value: crntAzureApiKey
}).then((apiKey: string) => {
setAzureApiKey(apiKey);
});
}
if (crntAzureRegion !== azureRegion) {
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
key: ExtensionState.Secrets.Azure.TranslatorRegion,
value: crntAzureRegion
}).then((apiKey: string) => {
setAzureRegion(apiKey);
});
}
}, [crntDeeplApiKey, deeplApiKey, crntAzureApiKey, azureApiKey, crntAzureRegion, azureRegion]);
const onChange = (key: string, value: string) => {
if (key === ExtensionState.Secrets.Deepl.ApiKey) {
setCrntDeeplApiKey(value);
} else if (key === ExtensionState.Secrets.Azure.TranslatorKey) {
setCrntAzureApiKey(value);
} else if (key === ExtensionState.Secrets.Azure.TranslatorRegion) {
setCrntAzureRegion(value);
}
};
React.useEffect(() => {
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.DeeplApiKey).then((apiKey: string) => {
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Deepl.ApiKey).then((apiKey: string) => {
setDeeplApiKey(apiKey);
setCrntDeeplApiKey(apiKey);
});
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Azure.TranslatorKey).then((apiKey: string) => {
setAzureApiKey(apiKey);
setCrntAzureApiKey(apiKey);
});
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Azure.TranslatorRegion).then((apiKey: string) => {
setAzureRegion(apiKey);
setCrntAzureRegion(apiKey);
});
}, []);
return (
@@ -39,16 +77,37 @@ export const IntegrationsView: React.FunctionComponent<IIntegrationsViewProps> =
<SettingsInput
label={l10n.t(LocalizationKey.settingsIntegrationsViewDeeplIntputLabel)}
name={ExtensionState.Secrets.DeeplApiKey}
name={ExtensionState.Secrets.Deepl.ApiKey}
value={crntDeeplApiKey || ""}
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewDeeplIntputPlaceholder)}
onChange={onChange}
/>
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsIntegrationsViewAzureTitle)}</h2>
<SettingsInput
label={l10n.t(LocalizationKey.settingsIntegrationsViewAzureIntputLabel)}
name={ExtensionState.Secrets.Azure.TranslatorKey}
value={crntAzureApiKey || ""}
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewAzureIntputPlaceholder)}
onChange={onChange}
/>
<SettingsInput
label={l10n.t(LocalizationKey.settingsIntegrationsViewAzureRegionLabel)}
name={ExtensionState.Secrets.Azure.TranslatorRegion}
value={crntAzureRegion || ""}
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewAzureRegionPlaceholder)}
onChange={onChange}
/>
<div className={`mt-4 flex gap-2`}>
<VSCodeButton
onClick={onSave}
disabled={deeplApiKey === crntDeeplApiKey}>
disabled={
deeplApiKey === crntDeeplApiKey &&
azureApiKey === crntAzureApiKey &&
azureRegion === crntAzureRegion
}>
{l10n.t(LocalizationKey.commonSave)}
</VSCodeButton>
</div>

View File

@@ -1,6 +1,6 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { GeneralCommands } from '../../../constants';
import { GeneralCommands, WEBSITE_LINKS } from '../../../constants';
import { SnippetInput } from './SnippetInput';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
@@ -30,7 +30,7 @@ export const NewForm: React.FunctionComponent<INewFormProps> = ({
const openLink = () => {
Messenger.send(
GeneralCommands.toVSCode.openLink,
'https://frontmatter.codes/docs/snippets#placeholders'
WEBSITE_LINKS.docs.snippetsPlaceholders
);
};

View File

@@ -42,6 +42,10 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
const insertPlaceholderValues = useCallback(
async (value: SnippetSpecialPlaceholders) => {
if (!value) {
return '';
}
if (value === 'FM_SELECTED_TEXT') {
return selection || '';
}
@@ -141,13 +145,23 @@ ${snippetBody}
const snippetFields = snippet.fields || [];
// Loop over all fields to check if they are present in the snippet
for (const field of snippetFields) {
console.log('placeholders', placeholders);
console.log('snippetFields', snippetFields);
for await (const field of snippetFields) {
console.log('field', field);
const idx = placeholders.findIndex((fieldName) => fieldName === field.name);
if (idx > -1) {
allFields.push({
...field,
value: await insertPlaceholderValues(field.default || '')
});
try {
const value = await insertPlaceholderValues(field.default || '');
console.log('value', value);
allFields.push({
...field,
value
});
} catch (e) {
console.log('Error', (e as Error).message)
}
}
}

View File

@@ -4,7 +4,7 @@ import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { FeatureFlag } from '../../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../../constants';
import { FEATURE_FLAG, WEBSITE_LINKS } from '../../../constants';
import { TelemetryEvent } from '../../../constants/TelemetryEvent';
import { SnippetParser } from '../../../helpers/SnippetParser';
import { DashboardMessage } from '../../DashboardMessage';
@@ -146,7 +146,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (
<p className="text-xl mt-4">
<a
className={`text-[var(--frontmatter-link)] hover:text-[var(--frontmatter-link-hover)]`}
href={`https://frontmatter.codes/docs/snippets`}
href={WEBSITE_LINKS.docs.snippets}
title={l10n.t(LocalizationKey.dashboardSnippetsViewSnippetsReadMore)}
>
{l10n.t(LocalizationKey.dashboardSnippetsViewSnippetsReadMore)}

View File

@@ -12,12 +12,12 @@ import {
MediaTotalAtom,
PageAtom,
SearchAtom,
SelectedMediaFolderAtom,
SettingsAtom
} from '../state';
import Fuse from 'fuse.js';
import usePagination from './usePagination';
import { usePrevious } from '../../panelWebView/hooks/usePrevious';
import useMediaFolder from './useMediaFolder';
const fuseOptions: Fuse.IFuseOptions<MediaInfo> = {
keys: [
@@ -35,7 +35,7 @@ export default function useMedia() {
// const page = useRecoilValue(PageAtom);
const [page, setPage] = useRecoilState(PageAtom);
const [searchedMedia, setSearchedMedia] = useState<MediaInfo[]>([]);
const [, setSelectedFolder] = useRecoilState(SelectedMediaFolderAtom);
const { updateFolder } = useMediaFolder();
const [, setTotal] = useRecoilState(MediaTotalAtom);
const [, setFolders] = useRecoilState(MediaFoldersAtom);
const [, setAllContentFolders] = useRecoilState(AllContentFoldersAtom);
@@ -79,7 +79,7 @@ export default function useMedia() {
setMedia(payload.media);
setTotal(payload.total);
setFolders(payload.folders);
setSelectedFolder(payload.selectedFolder);
updateFolder(payload.selectedFolder);
if (search) {
searchMedia(search, payload.media);
} else {

View File

@@ -0,0 +1,17 @@
import { useRecoilState } from 'recoil';
import { MultiSelectedItemsAtom, SelectedMediaFolderAtom } from '../state';
export default function useMediaFolder() {
const [selectedFolder, setSelectedFolder] = useRecoilState(SelectedMediaFolderAtom);
const [, setSelectedFiles] = useRecoilState(MultiSelectedItemsAtom);
const updateFolder = (folder: string) => {
setSelectedFolder(folder);
setSelectedFiles([]);
};
return {
selectedFolder,
updateFolder
};
}

View File

@@ -0,0 +1,91 @@
import { useMemo } from 'react';
import { MediaInfo } from '../../models';
import { dirname } from 'path';
import { useRecoilValue } from 'recoil';
import { SettingsSelector } from '../state';
import { parseWinPath } from '../../helpers/parseWinPath';
export default function useMediaInfo(media?: MediaInfo) {
const settings = useRecoilValue(SettingsSelector);
const mediaFolder = useMemo(() => {
if (settings?.wsFolder && media?.fsPath) {
let relPath = media.fsPath.split(settings.wsFolder).pop();
if (settings.staticFolder && relPath) {
relPath = relPath.split(settings.staticFolder).pop();
}
return dirname(parseWinPath(relPath) || '');
}
return '';
}, [media?.fsPath, settings?.staticFolder, settings?.wsFolder]);
const mediaSize = useMemo(() => {
if (media?.size) {
const size = media.size / (1024 * 1024);
if (size > 1) {
return `${size.toFixed(2)} MB`;
} else {
return `${(size * 1024).toFixed(2)} KB`;
}
}
return '';
}, [media]);
const mediaDimensions = useMemo(() => {
if (media?.dimensions) {
return `${media.dimensions.width} x ${media.dimensions.height}`;
}
return '';
}, [media]);
const mediaDetails = useMemo(() => {
let sizeDetails = [];
if (mediaDimensions) {
sizeDetails.push(mediaDimensions);
}
if (mediaSize) {
sizeDetails.push(mediaSize);
}
return sizeDetails.join(' - ');
}, [mediaDimensions, mediaSize]);
const isVideo = useMemo(() => {
if (media?.mimeType?.startsWith('video/')) {
return true;
}
return false;
}, [media]);
const isAudio = useMemo(() => {
if (media?.mimeType?.startsWith('audio/')) {
return true;
}
return false;
}, [media]);
const isImage = useMemo(() => {
if (
media?.mimeType?.startsWith('image/') &&
!media?.mimeType?.startsWith('image/vnd.adobe.photoshop')
) {
return true;
}
return false;
}, [media]);
return {
mediaFolder,
mediaSize,
mediaDimensions,
mediaDetails,
isVideo,
isAudio,
isImage
};
}

View File

@@ -15,7 +15,6 @@ import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { NavigationType } from '../models';
import { GeneralCommands } from '../../constants';
import * as l10n from '@vscode/l10n';
export default function useMessages() {
const [loading, setLoading] = useRecoilState(LoadingAtom);
@@ -25,7 +24,6 @@ export default function useMessages() {
const [, setMode] = useRecoilState(ModeAtom);
const [, setView] = useRecoilState(DashboardViewAtom);
const [, setSearchReady] = useRecoilState(SearchReadyAtom);
const [localeReady, setLocaleReady] = useState<boolean>(false);
const messageListener = (event: MessageEvent<EventData<any>>) => {
const message = event.data;
@@ -61,12 +59,6 @@ export default function useMessages() {
case GeneralCommands.toWebview.setMode:
setMode(message.payload);
break;
case GeneralCommands.toWebview.setLocalization:
l10n.config({
contents: message.payload
});
setLocaleReady(true);
break;
}
};
@@ -78,7 +70,6 @@ export default function useMessages() {
Messenger.send(DashboardMessage.getTheme);
Messenger.send(DashboardMessage.getData);
Messenger.send(DashboardMessage.getMode);
Messenger.send(GeneralCommands.toVSCode.getLocalization);
return () => {
Messenger.unlisten(messageListener);
@@ -89,7 +80,6 @@ export default function useMessages() {
loading,
pages,
viewData,
settings,
localeReady
settings
};
}

View File

@@ -22,7 +22,7 @@ import { DashboardMessage } from '../DashboardMessage';
import { EventData } from '@estruyf/vscode/dist/models';
import { parseWinPath } from '../../helpers/parseWinPath';
import { sortPages } from '../../utils/sortPages';
import { ExtensionState } from '../../constants';
import { ExtensionState, GeneralCommands } from '../../constants';
import { SortingOption } from '../models';
import { I18nConfig } from '../../models';
import { usePrevious } from '../../panelWebView/hooks/usePrevious';
@@ -204,7 +204,7 @@ export default function usePages(pages: Page[]) {
setTabInfo(draftTypes);
if (Object.keys(filters).length === 0) {
const availableFilters = (settings?.filters || []).filter((f) => f !== 'pageFolders' && f !== 'tags' && f !== 'categories');
const availableFilters = (settings?.filters || []).filter((f) => f !== 'contentFolders' && f !== 'tags' && f !== 'categories');
if (availableFilters.length > 0) {
const allFilters: { [filter: string]: string[]; } = {};
for (const filter of availableFilters) {
@@ -220,16 +220,6 @@ export default function usePages(pages: Page[]) {
}
}
if (tabPrevious !== tab || !locales || locales.length === 0) {
// Store the locale information
const config: I18nConfig[] = [];
crntPages.forEach((page) => {
if (page.fmLocale && !config.some(locale => locale.locale === page.fmLocale?.locale)) {
config.push(page.fmLocale);
}
});
setLocales(config);
}
// Set the pages
setPageItems(crntPages);
@@ -276,6 +266,12 @@ export default function usePages(pages: Page[]) {
} else {
startPageProcessing();
}
if (pages && pages.length > 0) {
messageHandler.request<I18nConfig[]>(GeneralCommands.toVSCode.content.locales).then((config) => {
setLocales(config || []);
});
}
}, [settings?.draftField, pages, sorting, search, tag, category, locale, filters, folder]);
useEffect(() => {

View File

@@ -0,0 +1,20 @@
import { useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { MultiSelectedItemsAtom } from '../state';
export default function useSelectedItems() {
const [selectedFiles, setSelectedFiles] = useRecoilState(MultiSelectedItemsAtom);
const onMultiSelect = useCallback((filePath: string) => {
if (selectedFiles.includes(filePath)) {
setSelectedFiles(selectedFiles.filter((file) => file !== filePath));
} else {
setSelectedFiles([...selectedFiles, filePath]);
}
}, [selectedFiles]);
return {
selectedFiles,
onMultiSelect
};
}

View File

@@ -3,8 +3,6 @@ import { render } from 'react-dom';
import { RecoilRoot } from 'recoil';
import { App } from './components/App';
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
import { SENTRY_LINK, SentryIgnore } from '../constants';
import { MemoryRouter } from 'react-router-dom';
import './styles.css';
import { Preview } from './components/Preview';
@@ -12,6 +10,9 @@ import { SettingsProvider } from './providers/SettingsProvider';
import { CustomPanelViewResult } from '../models';
import { Chatbot } from './components/Chatbot/Chatbot';
import { updateCssVariables } from './utils';
import { I10nProvider } from './providers/I10nProvider';
import { SentryInit } from '../utils/sentryInit';
import { WEBSITE_LINKS } from '../constants';
declare const acquireVsCodeApi: <T = unknown>() => {
getState: () => T;
@@ -50,7 +51,7 @@ export const routePaths: { [name: string]: string } = {
settings: '/settings',
};
const mutationObserver = new MutationObserver((mutationsList, observer) => {
const mutationObserver = new MutationObserver((_, __) => {
updateCssVariables();
});
@@ -64,19 +65,13 @@ if (elm) {
const url = elm?.getAttribute('data-url');
const experimental = elm?.getAttribute('data-experimental');
const webviewUrl = elm?.getAttribute('data-webview-url');
const isCrashDisabled = elm?.getAttribute('data-is-crash-disabled');
updateCssVariables();
mutationObserver.observe(document.body, { childList: false, attributes: true });
if (isProd === 'true') {
Sentry.init({
dsn: SENTRY_LINK,
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 0, // No performance tracing required
release: version || '',
environment: environment || '',
ignoreErrors: SentryIgnore
});
if (isProd === 'true' && isCrashDisabled === 'false') {
Sentry.init(SentryInit(version, environment));
Sentry.setTag("type", "dashboard");
if (document.body.getAttribute(`data-vscode-theme-id`)) {
@@ -88,17 +83,21 @@ if (elm) {
if (type === 'preview') {
render(
<SettingsProvider experimental={experimental === 'true'} version={version || ""}>
<Preview url={url} />
</SettingsProvider>, elm);
<I10nProvider>
<SettingsProvider experimental={experimental === 'true'} version={version || ""}>
<Preview url={url} />
</SettingsProvider>
</I10nProvider>, elm);
} else if (type === 'chatbot') {
render(
<SettingsProvider
aiUrl='https://frontmatter.codes'
experimental={experimental === 'true'}
version={version || ""}>
<Chatbot />
</SettingsProvider>, elm);
<I10nProvider>
<SettingsProvider
aiUrl={WEBSITE_LINKS.root}
experimental={experimental === 'true'}
version={version || ""}>
<Chatbot />
</SettingsProvider>
</I10nProvider>, elm);
} else {
render(
<RecoilRoot>
@@ -106,9 +105,11 @@ if (elm) {
initialEntries={Object.keys(routePaths).map((key: string) => routePaths[key]) as string[]}
initialIndex={1}
>
<SettingsProvider experimental={experimental === 'true'} version={version || ""} webviewUrl={webviewUrl || ""}>
<App showWelcome={!!welcome} />
</SettingsProvider>
<I10nProvider>
<SettingsProvider experimental={experimental === 'true'} version={version || ""} webviewUrl={webviewUrl || ""}>
<App showWelcome={!!welcome} />
</SettingsProvider>
</I10nProvider>
</MemoryRouter>
</RecoilRoot>,
elm

View File

@@ -6,6 +6,7 @@ import {
CustomScript,
CustomTaxonomy,
DraftField,
FilterType,
Framework,
GitSettings,
MediaContentType,
@@ -38,7 +39,7 @@ export interface Settings {
framework: Framework | null | undefined;
draftField: DraftField | null | undefined;
customSorting: SortingSetting[] | undefined;
filters: (string | { title: string; name: string })[] | undefined;
filters: (FilterType | { title: string; name: string })[] | undefined;
dashboardState: DashboardState;
scripts: CustomScript[];
dataFiles: DataFile[] | undefined;

View File

@@ -0,0 +1,36 @@
import * as React from 'react';
import { Page } from '../models';
import { MediaInfo } from '../../models';
interface IFilesProviderProps {
files: Page[] | MediaInfo[];
}
const FilesContext = React.createContext<IFilesProviderProps | undefined>(undefined);
const FilesProvider: React.FunctionComponent<IFilesProviderProps> = ({ files, children }: React.PropsWithChildren<IFilesProviderProps>) => {
return (
<FilesContext.Provider
value={{
files
}}
>
{children}
</FilesContext.Provider>
)
};
const useFilesContext = (): IFilesProviderProps => {
const loadFunc = React.useContext(FilesContext);
if (loadFunc === undefined) {
throw new Error('useFilesContext must be used within the FilesProvider');
}
return loadFunc;
};
FilesContext.displayName = 'FilesContext';
FilesProvider.displayName = 'FilesProvider';
export { FilesProvider, useFilesContext };

View File

@@ -0,0 +1,52 @@
import * as React from 'react';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { GeneralCommands } from '../../constants';
import * as l10n from '@vscode/l10n';
interface I10nProviderProps { }
const I10nContext = React.createContext<I10nProviderProps | undefined>(undefined);
const I10nProvider: React.FunctionComponent<I10nProviderProps> = ({ children }: React.PropsWithChildren<I10nProviderProps>) => {
const [localeReady, setLocaleReady] = React.useState<boolean>(false);
React.useEffect(() => {
messageHandler.request<any>(GeneralCommands.toVSCode.getLocalization).then((contents) => {
if (contents) {
l10n.config({
contents
});
setTimeout(() => {
setLocaleReady(true);
}, 0);
}
}).catch(() => {
setLocaleReady(false);
throw new Error('Error getting localization');
});
}, []);
return (
<I10nContext.Provider value={{}}>
{
localeReady && children
}
</I10nContext.Provider>
)
};
const useI10nContext = (): I10nProviderProps => {
const loadFunc = React.useContext(I10nContext);
if (loadFunc === undefined) {
throw new Error('useI10nContext must be used within the I10nProvider');
}
return loadFunc;
};
I10nContext.displayName = 'I10nContext';
I10nProvider.displayName = 'I10nProvider';
export { I10nProvider, useI10nContext };

View File

@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const MultiSelectedItemsAtom = atom<string[]>({
key: 'MultiSelectedItemsAtom',
default: []
});

View File

@@ -0,0 +1,12 @@
import { atom } from 'recoil';
export const SelectedItemActionAtom = atom<
| {
path: string;
action: 'view' | 'edit';
}
| undefined
>({
key: 'SelectedItemActionAtom',
default: undefined
});

View File

@@ -14,10 +14,12 @@ export * from './LocalesAtom';
export * from './MediaFoldersAtom';
export * from './MediaTotalAtom';
export * from './ModeAtom';
export * from './MultiSelectedItemsAtom';
export * from './PageAtom';
export * from './PinnedItems';
export * from './SearchAtom';
export * from './SearchReadyAtom';
export * from './SelectedItemActionAtom';
export * from './SelectedMediaFolderAtom';
export * from './SettingsAtom';
export * from './SortingAtom';

View File

@@ -411,7 +411,7 @@
}
.question {
@apply relative ml-auto mr-3 w-5/6 rounded-full rounded-br-none bg-teal-900 py-2 px-4 text-whisper-500;
@apply relative ml-auto mr-3 w-5/6 rounded-full rounded-br-none bg-teal-900 px-4 py-2 text-whisper-500;
&:after {
--size: 1rem;

View File

@@ -52,7 +52,12 @@ export const updateCssVariables = () => {
);
// Borders
const borderColor = styles.getPropertyValue('--vscode-panel-border');
document.documentElement.style.setProperty('--frontmatter-border', 'var(--vscode-panel-border)');
document.documentElement.style.setProperty(
'--frontmatter-border-preserve',
preserveColor(borderColor) || 'var(--vscode-panel-border)'
);
// Other colors which should be preserved (no opacity)
const buttonBackground = styles.getPropertyValue('--vscode-button-background');

View File

@@ -18,7 +18,6 @@ import {
SETTING_SITE_BASEURL,
SETTING_TAXONOMY_CONTENT_TYPES,
SETTING_TEMPLATES_PREFIX,
SETTING_MODIFIED_FIELD,
DefaultFieldValues
} from '../constants';
import { DumpOptions } from 'js-yaml';
@@ -40,7 +39,7 @@ import { Article } from '../commands';
import { join, parse as parseFile } from 'path';
import { EditorHelper } from '@estruyf/vscode';
import sanitize from '../helpers/Sanitize';
import { ContentType as IContentType } from '../models';
import { Field, ContentType as IContentType } from '../models';
import { DateHelper } from './DateHelper';
import { DiagnosticSeverity, Position, window, Range } from 'vscode';
import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
@@ -379,7 +378,7 @@ export class ArticleHelper {
* @param article
* @returns
*/
public static getModifiedDateField(article: ParsedFrontMatter | null) {
public static getModifiedDateField(article: ParsedFrontMatter | null): Field | undefined {
if (!article || !article.data) {
return;
}
@@ -387,11 +386,7 @@ export class ArticleHelper {
const articleCt = ArticleHelper.getContentType(article);
const modDateField = articleCt.fields.find((f) => f.isModifiedDate);
return (
modDateField?.name ||
(Settings.get(SETTING_MODIFIED_FIELD) as string) ||
DefaultFields.LastModified
);
return modDateField;
}
/**
@@ -661,16 +656,19 @@ export class ArticleHelper {
}
}
const regex = new RegExp(`{{${placeholder.id}}}`, 'g');
let updatedValue = filePath
? await processArticlePlaceholdersFromPath(placeHolderValue, filePath)
: placeHolderValue;
let updatedValue = placeHolderValue;
// Check if the file already exists, during creation it might not exist yet
if (filePath && (await existsAsync(filePath))) {
updatedValue = await processArticlePlaceholdersFromPath(placeHolderValue, filePath);
}
updatedValue = processTimePlaceholders(updatedValue, dateFormat);
if (value === `{{${placeholder.id}}}`) {
value = updatedValue;
} else {
const regex = new RegExp(`{{${placeholder.id}}}`, 'g');
value = value.replace(regex, updatedValue);
}
} catch (e) {

View File

@@ -95,7 +95,7 @@ export class ContentType {
const contentTypes = ContentType.getAll();
const folders = Folders.get().filter((f) => !f.disableCreation);
const folder = folders.find((f) => f.title === selectedFolder);
const folder = folders.find((f) => f.path === selectedFolder.path);
if (!folder) {
return;
@@ -937,6 +937,13 @@ export class ContentType {
contentType
);
let isTypeSet = false;
if (data.type) {
isTypeSet = true;
} else {
data.type = contentType.name;
}
const article: ParsedFrontMatter = {
content: '',
data: Object.assign({}, data),
@@ -945,6 +952,10 @@ export class ContentType {
data = ArticleHelper.updateDates(article);
if (isTypeSet) {
delete data.type;
}
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
data['type'] = contentType.name;
}

View File

@@ -42,6 +42,7 @@ import {
CustomScript,
DEFAULT_MEDIA_CONTENT_TYPE,
DraftField,
FilterType,
MediaContentType,
Snippets,
SortingSetting,
@@ -111,7 +112,8 @@ export class DashboardSettings {
draftField: Settings.get<DraftField>(SETTING_CONTENT_DRAFT_FIELD),
customSorting: Settings.get<SortingSetting[]>(SETTING_CONTENT_SORTING),
contentFolders: Folders.get(),
filters: Settings.get<string[]>(SETTING_CONTENT_FILTERS),
filters:
Settings.get<(FilterType | { title: string; name: string })[]>(SETTING_CONTENT_FILTERS),
crntFramework: Settings.get<string>(SETTING_FRAMEWORK_ID),
framework: !isInitialized && wsFolder ? await FrameworkDetector.get(wsFolder.fsPath) : null,
scripts: Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || [],

View File

@@ -15,20 +15,15 @@ import { Template } from '../commands/Template';
import {
EXTENSION_NAME,
GITHUB_LINK,
SETTING_DATE_FIELD,
SETTING_MODIFIED_FIELD,
EXTENSION_BETA_ID,
EXTENSION_ID,
ExtensionState,
CONFIG_KEY,
SETTING_CONTENT_PAGE_FOLDERS,
SETTING_DASHBOARD_MEDIA_SNIPPET,
SETTING_CONTENT_SNIPPETS,
SETTING_TEMPLATES_ENABLED,
SETTING_TAXONOMY_TAGS,
SETTING_TAXONOMY_CATEGORIES
} from '../constants';
import { ContentFolder, Snippet, TaxonomyType } from '../models';
import { ContentFolder, TaxonomyType } from '../models';
import { Notifications } from './Notifications';
import { Settings } from './SettingsHelper';
import { TaxonomyHelper } from './TaxonomyHelper';
@@ -202,64 +197,6 @@ export class Extension {
await Settings.createTeamSettings();
}
const hideDateDeprecation = await Extension.getInstance().getState<boolean>(
ExtensionState.Updates.v7_0_0.dateFields,
'workspace'
);
if (!hideDateDeprecation) {
// Migration scripts can be written here
const publishField = Settings.inspect(SETTING_DATE_FIELD);
const modifiedField = Settings.inspect(SETTING_MODIFIED_FIELD);
// Check for extension deprecations
if (
publishField?.workspaceValue ||
publishField?.globalValue ||
publishField?.teamValue ||
modifiedField?.workspaceValue ||
modifiedField?.globalValue ||
modifiedField?.teamValue
) {
Notifications.warning(
l10n.t(
LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarning,
`${CONFIG_KEY}.${SETTING_DATE_FIELD}`,
`${CONFIG_KEY}.${SETTING_MODIFIED_FIELD}`
),
l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningHide),
l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide)
).then(async (value) => {
if (
value ===
l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide)
) {
const isProd = this.isProductionMode;
commands.executeCommand(
'vscode.open',
Uri.parse(
`https://${
isProd ? '' : 'beta.'
}frontmatter.codes/docs/troubleshooting#publish-and-modified-date-migration`
)
);
await Extension.getInstance().setState<boolean>(
ExtensionState.Updates.v7_0_0.dateFields,
true,
'workspace'
);
} else if (
value === l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningHide)
) {
await Extension.getInstance().setState<boolean>(
ExtensionState.Updates.v7_0_0.dateFields,
true,
'workspace'
);
}
});
}
}
if (major < 7) {
const contentFolders: ContentFolder[] = Settings.get(
SETTING_CONTENT_PAGE_FOLDERS
@@ -282,28 +219,6 @@ export class Extension {
}
if (major <= 7 && minor < 3) {
const mediaSnippet = Settings.get<string[]>(SETTING_DASHBOARD_MEDIA_SNIPPET);
if (mediaSnippet && mediaSnippet.length > 0) {
let snippet = mediaSnippet.join(`\n`);
snippet = snippet.replace(`{mediaUrl}`, `[[&mediaUrl]]`);
snippet = snippet.replace(`{mediaHeight}`, `[[mediaHeight]]`);
snippet = snippet.replace(`{mediaWidth}`, `[[mediaWidth]]`);
snippet = snippet.replace(`{caption}`, `[[&caption]]`);
snippet = snippet.replace(`{alt}`, `[[alt]]`);
snippet = snippet.replace(`{filename}`, `[[filename]]`);
snippet = snippet.replace(`{title}`, `[[title]]`);
const snippets = Settings.get<Snippet[]>(SETTING_CONTENT_SNIPPETS) || ({} as any);
snippets[`Media snippet (migrated)`] = {
body: snippet.split(`\n`),
isMediaSnippet: true,
description: `Migrated media snippet from frontMatter.dashboard.mediaSnippet setting`
};
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
}
const templates = await Template.getTemplates();
if (templates && templates.length > 0) {
const answer = await window.showQuickPick(

View File

@@ -8,6 +8,12 @@ import { Logger } from './Logger';
import { SponsorAi } from '../services/SponsorAI';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../localization';
import { ContentFolder } from '../models';
interface FolderQuickPickItem extends QuickPickItem {
path: string;
locale?: string;
}
export class Questions {
/**
@@ -124,13 +130,27 @@ export class Questions {
*/
public static async SelectContentFolder(
showWarning: boolean = true
): Promise<string | undefined> {
): Promise<FolderQuickPickItem | undefined> {
let folders = Folders.get().filter((f) => !f.disableCreation);
let selectedFolder: string | undefined;
let selectedFolder: FolderQuickPickItem | undefined;
if (folders.length > 1) {
const folderOptions = folders.map((f: ContentFolder) => {
if (f.locale) {
return {
label: `${f.title} (${f.localeTitle || f.locale})`,
locale: f.locale,
path: f.path
} as FolderQuickPickItem;
}
return {
label: f.title,
path: f.path
} as FolderQuickPickItem;
});
selectedFolder = await window.showQuickPick(
folders.map((f) => f.title),
folderOptions,
{
title: l10n.t(LocalizationKey.helpersQuestionsSelectContentFolderQuickPickTitle),
placeHolder: l10n.t(
@@ -140,7 +160,10 @@ export class Questions {
}
);
} else if (folders.length === 1) {
selectedFolder = folders[0].title;
selectedFolder = {
label: folders[0].title,
path: folders[0].path
} as FolderQuickPickItem;
} else {
// When no page folders are found, the welcome dashboard is shown
return;
@@ -189,6 +212,11 @@ export class Questions {
label: contentType.name
}));
if (options.length === 0) {
Notifications.error(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickErrorNoContentTypes);
return;
}
const selectedOption = await window.showQuickPick(options, {
title: l10n.t(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickTitle),
placeHolder: l10n.t(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickPlaceholder),

View File

@@ -43,7 +43,8 @@ import {
SETTING_CONFIG_DYNAMIC_FILE_PATH,
SETTING_PROJECTS,
SETTING_TAXONOMY_TAGS,
SETTING_TAXONOMY_CATEGORIES
SETTING_TAXONOMY_CATEGORIES,
SETTING_CONTENT_FILTERS
} from '../constants';
import { Folders } from '../commands/Folders';
import { join, basename, dirname, parse } from 'path';
@@ -804,7 +805,8 @@ export class Settings {
settingName === SETTING_GLOBAL_NOTIFICATIONS ||
settingName === SETTING_GLOBAL_NOTIFICATIONS_DISABLED ||
settingName === SETTING_MEDIA_SUPPORTED_MIMETYPES ||
settingName === SETTING_COMMA_SEPARATED_FIELDS
settingName === SETTING_COMMA_SEPARATED_FIELDS ||
settingName === SETTING_CONTENT_FILTERS
) {
if (typeof originalConfig[key] === 'undefined') {
Settings.globalConfig[key] = value;

View File

@@ -24,6 +24,9 @@ export class SlugHelper {
if (slugTemplate) {
if (slugTemplate.includes('{{title}}')) {
const regex = new RegExp('{{title}}', 'g');
slugTemplate = slugTemplate.replace(regex, articleTitle.toLowerCase().replace(/\s/g, '-'));
} else if (slugTemplate.includes('{{seoTitle}}')) {
const regex = new RegExp('{{seoTitle}}', 'g');
slugTemplate = slugTemplate.replace(regex, SlugHelper.slugify(articleTitle));
}

View File

@@ -1,7 +1,11 @@
import { workspace } from 'vscode';
import { Extension, Settings } from '.';
import { EXTENSION_BETA_ID, EXTENSION_ID, SETTING_TELEMETRY_DISABLE } from '../constants';
const METRICS_URL = 'https://frontmatter.codes/api/metrics';
import {
EXTENSION_BETA_ID,
EXTENSION_ID,
SETTING_TELEMETRY_DISABLE,
WEBSITE_LINKS
} from '../constants';
export class Telemetry {
private static instance: Telemetry;
@@ -23,6 +27,24 @@ export class Telemetry {
return Telemetry.instance;
}
public static isVscodeEnabled(): boolean {
const config = workspace.getConfiguration('telemetry');
const isVscodeEnable = config.get<'off' | undefined>('enableTelemetry');
return isVscodeEnable === 'off' ? false : true;
}
/**
* Checks if telemetry is enabled.
* @returns {boolean} Returns true if telemetry is enabled, false otherwise.
*/
public static isEnabled(): boolean {
const isVscodeEnable = Telemetry.isVscodeEnabled();
const isDisabled = Settings.get<boolean>(SETTING_TELEMETRY_DISABLE);
return isDisabled || isVscodeEnable ? false : true;
}
/**
* Send metrics to our own database
* @param eventName
@@ -30,8 +52,7 @@ export class Telemetry {
* @returns
*/
public static send(eventName: string, properties?: any) {
const isDisabled = Settings.get<boolean>(SETTING_TELEMETRY_DISABLE);
if (isDisabled) {
if (!Telemetry.isEnabled()) {
return;
}
@@ -59,7 +80,7 @@ export class Telemetry {
// Set a new timeout
instance.timeout = setTimeout(async () => {
await fetch(METRICS_URL, {
await fetch(WEBSITE_LINKS.api.metrics, {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@@ -2,6 +2,7 @@ import { GeneralCommands } from '../../constants';
import { PostMessageData } from '../../models';
import { BaseListener } from './BaseListener';
import { getLocalizationFile } from '../../utils/getLocalizationFile';
import { i18n } from '../../commands/i18n';
export class LocalizationListener extends BaseListener {
/**
@@ -11,14 +12,29 @@ export class LocalizationListener extends BaseListener {
public static process(msg: PostMessageData) {
switch (msg.command) {
case GeneralCommands.toVSCode.getLocalization:
this.getLocalization();
this.getLocalization(msg.command, msg.requestId);
break;
case GeneralCommands.toVSCode.content.locales:
this.getContentLocales(msg.command, msg.requestId);
break;
}
}
public static async getLocalization() {
const fileContents = await getLocalizationFile();
public static async getLocalization(command: string, requestId?: string) {
if (!command || !requestId) {
return;
}
this.sendMsg(GeneralCommands.toWebview.setLocalization as any, fileContents);
const fileContents = await getLocalizationFile();
this.sendRequest(command as any, requestId, fileContents);
}
private static async getContentLocales(command: string, requestId?: string) {
if (!command || !requestId) {
return;
}
const config = i18n.getAll();
this.sendRequest(command as any, requestId, config);
}
}

View File

@@ -140,7 +140,7 @@ export class SnippetListener extends BaseListener {
data: { value: string; filePath: string },
requestId?: string
) {
if (!data.value || !command || !requestId) {
if (!command || !requestId) {
return;
}

View File

@@ -306,7 +306,6 @@ export class DataListener extends BaseListener {
}
}
const dateFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'datetime');
const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image');
const fileFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'file');
const fieldsWithEmojiEncoding = contentType.fields.filter((f) => f.encodeEmoji);
@@ -314,13 +313,6 @@ export class DataListener extends BaseListener {
// Support multi-level fields
const parentObj = DataListener.getParentObject(article.data, article, parents, blockData);
const dateFieldsArray = dateFields.find((f: Field[]) => {
const lastField = f?.[f.length - 1];
if (lastField) {
return lastField.name === field;
}
});
// Check multi-image fields
const multiImageFieldsArray = imageFields.find((f: Field[]) => {
const lastField = f?.[f.length - 1];
@@ -338,13 +330,7 @@ export class DataListener extends BaseListener {
});
// Check date fields
if (dateFieldsArray && dateFieldsArray.length > 0) {
for (const dateField of dateFieldsArray) {
if (field === dateField.name && value) {
parentObj[field] = Article.formatDate(new Date(value), dateField.dateFormat);
}
}
} else if (multiImageFieldsArray || multiFileFieldsArray) {
if (multiImageFieldsArray || multiFileFieldsArray) {
const fields =
multiImageFieldsArray && multiImageFieldsArray.length > 0
? multiImageFieldsArray

View File

@@ -11,14 +11,17 @@ export class LocalizationListener extends BaseListener {
public static process(msg: PostMessageData) {
switch (msg.command) {
case GeneralCommands.toVSCode.getLocalization:
this.getLocalization();
this.getLocalization(msg.command, msg.requestId);
break;
}
}
public static async getLocalization() {
const fileContents = await getLocalizationFile();
public static async getLocalization(command: string, requestId?: string) {
if (!command || !requestId) {
return;
}
this.sendMsg(GeneralCommands.toWebview.setLocalization as any, fileContents);
const fileContents = await getLocalizationFile();
this.sendRequest(command, requestId, fileContents);
}
}

View File

@@ -151,6 +151,22 @@ export enum LocalizationKey {
* Open: {0}
*/
commonOpenWithValue = 'common.openWithValue',
/**
* View
*/
commonView = 'common.view',
/**
* Translate
*/
commonTranslate = 'common.translate',
/**
* Languages
*/
commonLanguages = 'common.languages',
/**
* Scripts
*/
commonScripts = 'common.scripts',
/**
* Loading content
*/
@@ -248,13 +264,33 @@ export enum LocalizationKey {
*/
settingsIntegrationsViewDeeplTitle = 'settings.integrationsView.deepl.title',
/**
* Authentication key
* API key
*/
settingsIntegrationsViewDeeplIntputLabel = 'settings.integrationsView.deepl.intput.label',
/**
* Enter your DeepL authentication key
* Enter your Azure Translator API key
*/
settingsIntegrationsViewDeeplIntputPlaceholder = 'settings.integrationsView.deepl.intput.placeholder',
/**
* Azure AI Translator Service
*/
settingsIntegrationsViewAzureTitle = 'settings.integrationsView.azure.title',
/**
* Subscription key
*/
settingsIntegrationsViewAzureIntputLabel = 'settings.integrationsView.azure.intput.label',
/**
* Enter your Azure AI Translator - Subscription key
*/
settingsIntegrationsViewAzureIntputPlaceholder = 'settings.integrationsView.azure.intput.placeholder',
/**
* Region
*/
settingsIntegrationsViewAzureRegionLabel = 'settings.integrationsView.azure.region.label',
/**
* Enter your Azure AI Translator - Region. Example: westeurope
*/
settingsIntegrationsViewAzureRegionPlaceholder = 'settings.integrationsView.azure.region.placeholder',
/**
* Developer mode
*/
@@ -463,6 +499,18 @@ export enum LocalizationKey {
* All
*/
dashboardFiltersLanguageFilterAll = 'dashboard.filters.languageFilter.all',
/**
* {0} selected
*/
dashboardHeaderActionsBarItemsSelected = 'dashboard.header.actionsBar.itemsSelected',
/**
* Delete selected files
*/
dashboardHeaderActionsBarAlertDeleteTitle = 'dashboard.header.actionsBar.alertDelete.title',
/**
* Are you sure you want to delete the selected files?
*/
dashboardHeaderActionsBarAlertDeleteDescription = 'dashboard.header.actionsBar.alertDelete.description',
/**
* Home
*/
@@ -719,6 +767,14 @@ export enum LocalizationKey {
* Create new folder
*/
dashboardMediaFolderCreationFolderCreate = 'dashboard.media.folderCreation.folder.create',
/**
* Content directory
*/
dashboardMediaFolderItemContentDirectory = 'dashboard.media.folderItem.contentDirectory',
/**
* Public directory
*/
dashboardMediaFolderItemPublicDirectory = 'dashboard.media.folderItem.publicDirectory',
/**
* Insert image
*/
@@ -1725,9 +1781,17 @@ export enum LocalizationKey {
*/
commandsI18nCreateWarningNoConfig = 'commands.i18n.create.warning.noConfig',
/**
* The current file cannot be used for i18n content creation.
* Could not retrieve the locale for the current file.
*/
commandsI18nCreateWarningNotDefaultLocale = 'commands.i18n.create.warning.notDefaultLocale',
commandsI18nCreateErrorNoLocaleDefinition = 'commands.i18n.create.error.noLocaleDefinition',
/**
* Current file has been translated to all available languages.
*/
commandsI18nCreateErrorNoLocales = 'commands.i18n.create.error.noLocales',
/**
* Could not define a content folder for the current file.
*/
commandsI18nCreateErrorNoContentFolder = 'commands.i18n.create.error.noContentFolder',
/**
* The i18n translation already exists.
*/
@@ -2148,18 +2212,6 @@ export enum LocalizationKey {
* {0} has been updated to v{1} — check out what's new!
*/
helpersExtensionGetVersionUpdateNotification = 'helpers.extension.getVersion.update.notification',
/**
* The "{0}" and "{1}" settings have been deprecated. Please use the "isPublishDate" and "isModifiedDate" datetime field properties instead.
*/
helpersExtensionMigrateSettingsDeprecatedWarning = 'helpers.extension.migrateSettings.deprecated.warning',
/**
* Hide
*/
helpersExtensionMigrateSettingsDeprecatedWarningHide = 'helpers.extension.migrateSettings.deprecated.warning.hide',
/**
* See migration guide
*/
helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide = 'helpers.extension.migrateSettings.deprecated.warning.seeGuide',
/**
* {0} - Templates
*/
@@ -2280,6 +2332,10 @@ export enum LocalizationKey {
* No content type was selected.
*/
helpersQuestionsSelectContentTypeNoSelectionWarning = 'helpers.questions.selectContentType.noSelection.warning',
/**
* There are no matching content types configured for this folder.
*/
helpersQuestionsSelectContentTypeQuickPickErrorNoContentTypes = 'helpers.questions.selectContentType.quickPick.error.noContentTypes',
/**
* Article {0} is longer than {1} characters (current length: {2}). For SEO reasons, it would be better to make it less than {1} characters.
*/

View File

@@ -12,6 +12,10 @@ export interface ContentFolder {
originalPath?: string;
$schema?: string;
extended?: boolean;
locale?: string;
localeTitle?: string;
localeSourcePath?: string;
defaultLocale?: string;
locales: I18nConfig[];
}

1
src/models/FilterType.ts Normal file
View File

@@ -0,0 +1 @@
export type FilterType = 'contentFolders' | 'tags' | 'categories';

View File

@@ -191,6 +191,8 @@ export interface FolderInfo {
title: string;
files: number;
lastModified: FileInfo[];
locale?: string;
localeTitle?: string;
}
export interface FileInfo extends FileStat {

View File

@@ -11,6 +11,7 @@ export * from './DataFile';
export * from './DataFolder';
export * from './DataType';
export * from './DraftField';
export * from './FilterType';
export * from './Framework';
export * from './GitRepository';
export * from './GitSettings';

View File

@@ -286,7 +286,9 @@ export class PanelProvider implements WebviewViewProvider, Disposable {
<body>
<div id="app" data-isProd="${isProd}" data-environment="${
isBeta ? 'BETA' : 'main'
}" data-version="${version.usedVersion}"></div>
}" data-version="${
version.usedVersion
}" data-is-crash-disabled="${!Telemetry.isVscodeEnabled()}"></div>
${(scriptsToLoad || [])
.map((script) => {

View File

@@ -33,7 +33,6 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
folderAndFiles,
focusElm,
unsetFocus,
localeReady,
mode
} = useMessages();
const prevMediaSelection = usePrevious(mediaSelecting);
@@ -83,7 +82,7 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
);
}
if (loading && !localeReady) {
if (loading) {
return <Spinner />;
}

View File

@@ -1,14 +1,7 @@
import * as React from 'react';
import {
VsTable,
VsTableBody,
VsTableHeader,
VsTableHeaderCell,
VsTableRow,
VsTableCell
} from './VscodeComponents';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { VSCodeTable, VSCodeTableBody, VSCodeTableCell, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable';
export interface IArticleDetailsProps {
details: {
@@ -32,52 +25,55 @@ const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({
<div className={`seo__status__details valid`}>
<h4>{l10n.t(LocalizationKey.panelArticleDetailsTitle)}</h4>
<VsTable bordered>
<VsTableHeader slot="header">
<VsTableHeaderCell>
{l10n.t(LocalizationKey.panelArticleDetailsType)}
</VsTableHeaderCell>
<VsTableHeaderCell>
{l10n.t(LocalizationKey.panelArticleDetailsTotal)}
</VsTableHeaderCell>
</VsTableHeader>
<VsTableBody slot="body">
<VSCodeTable>
<VSCodeTableHeader>
<VSCodeTableRow>
<VSCodeTableHead>
{l10n.t(LocalizationKey.panelArticleDetailsType)}
</VSCodeTableHead>
<VSCodeTableHead>
{l10n.t(LocalizationKey.panelArticleDetailsTotal)}
</VSCodeTableHead>
</VSCodeTableRow>
</VSCodeTableHeader>
<VSCodeTableBody>
{details?.headings !== undefined && (
<VsTableRow>
<VsTableCell>{l10n.t(LocalizationKey.panelArticleDetailsHeadings)}</VsTableCell>
<VsTableCell>{details.headings}</VsTableCell>
</VsTableRow>
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsHeadings)}</VSCodeTableCell>
<VSCodeTableCell>{details.headings}</VSCodeTableCell>
</VSCodeTableRow>
)}
{details?.paragraphs !== undefined && (
<VsTableRow>
<VsTableCell>{l10n.t(LocalizationKey.panelArticleDetailsParagraphs)}</VsTableCell>
<VsTableCell>{details.paragraphs}</VsTableCell>
</VsTableRow>
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsParagraphs)}</VSCodeTableCell>
<VSCodeTableCell>{details.paragraphs}</VSCodeTableCell>
</VSCodeTableRow>
)}
{details?.internalLinks !== undefined && (
<VsTableRow>
<VsTableCell>{l10n.t(LocalizationKey.panelArticleDetailsInternalLinks)}</VsTableCell>
<VsTableCell>{details.internalLinks}</VsTableCell>
</VsTableRow>
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsInternalLinks)}</VSCodeTableCell>
<VSCodeTableCell>{details.internalLinks}</VSCodeTableCell>
</VSCodeTableRow>
)}
{details?.externalLinks !== undefined && (
<VsTableRow>
<VsTableCell>{l10n.t(LocalizationKey.panelArticleDetailsExternalLinks)}</VsTableCell>
<VsTableCell>{details.externalLinks}</VsTableCell>
</VsTableRow>
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsExternalLinks)}</VSCodeTableCell>
<VSCodeTableCell>{details.externalLinks}</VSCodeTableCell>
</VSCodeTableRow>
)}
{details?.images !== undefined && (
<VsTableRow>
<VsTableCell>{l10n.t(LocalizationKey.panelArticleDetailsImages)}</VsTableCell>
<VsTableCell>{details.images}</VsTableCell>
</VsTableRow>
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsImages)}</VSCodeTableCell>
<VSCodeTableCell>{details.images}</VSCodeTableCell>
</VSCodeTableRow>
)}
</VsTableBody>
</VsTable>
</VSCodeTableBody>
</VSCodeTable>
</div>
);
};

View File

@@ -66,7 +66,7 @@ const Collapsible: React.FunctionComponent<ICollapsibleProps> = ({
return (
<VsCollapsible title={title} onClick={triggerClick} open={isOpen}>
<div className={`section collapsible__body ${className || ''}`} slot="body">
<div className={`section collapsible__body ${className || ''}`}>
{children}
</div>
</VsCollapsible>

View File

@@ -5,9 +5,9 @@ import { useMemo } from 'react';
import { Field } from '../../../models';
import { CommandToCode } from '../../CommandToCode';
import { IMetadata } from '../Metadata';
import { VsLabel } from '../VscodeComponents';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { VSCodeLabel } from '../VSCode';
export interface IContentTypeValidatorProps {
fields: Field[];
@@ -50,7 +50,7 @@ export const ContentTypeValidator: React.FunctionComponent<IContentTypeValidator
return (
<div className="hint">
<VsLabel>
<VSCodeLabel>
<div className={`metadata_field__label metadata_field__alert`}>
<svg
width="16"
@@ -70,7 +70,7 @@ export const ContentTypeValidator: React.FunctionComponent<IContentTypeValidator
{l10n.t(LocalizationKey.panelContentTypeContentTypeValidatorTitle)}
</span>
</div>
</VsLabel>
</VSCodeLabel>
{l10n.t(LocalizationKey.panelContentTypeContentTypeValidatorHint).split(`\n`).map(s => (<p className="inline_hint" key={s}>{s}</p>))}

View File

@@ -95,6 +95,13 @@ export const DataBlockField: React.FunctionComponent<IDataBlockFieldProps> = ({
// Delete the field group to have it added at the end
delete data['fieldGroup'];
// Remove the empty fields
Object.keys(data).forEach((key) => {
if (data[key] === undefined || data[key] === null || Object.keys(data[key]).length === 0) {
delete data[key];
}
});
if (selectedIndex !== null && selectedIndex !== undefined && dataClone.length > 0) {
dataClone[selectedIndex] = {
...data,
@@ -306,7 +313,7 @@ export const DataBlockField: React.FunctionComponent<IDataBlockFieldProps> = ({
{selectedGroup?.fields &&
fieldsRenderer(
selectedGroup?.fields,
selectedBlockData || {},
Object.assign({}, selectedBlockData) || {},
[...parentFields, field.name],
{
parentFields: [...parentFields, field.name],

View File

@@ -1,12 +1,12 @@
import { RectangleStackIcon, PlusIcon } from '@heroicons/react/24/outline';
import { RectangleStackIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { VsLabel } from '../VscodeComponents';
import { DataBlockRecord } from '.';
import { SortableContainer, SortEnd } from 'react-sortable-hoc';
import { useCallback } from 'react';
import { FieldGroup } from '../../../models';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { VSCodeLabel } from '../VSCode';
export interface IDataBlockRecordsProps {
fieldGroups?: FieldGroup[];
@@ -52,7 +52,7 @@ export const DataBlockRecords = ({
return (
<div className="json_data__list">
<VsLabel>
<VSCodeLabel>
<div className={`metadata_field__label`}>
<div>
<RectangleStackIcon style={{ width: '16px', height: '16px' }} />
@@ -61,7 +61,7 @@ export const DataBlockRecords = ({
</span>
</div>
</div>
</VsLabel>
</VSCodeLabel>
<Container onSortEnd={onSort} useDragHandle>
{records.map((v: any, idx: number) => (

View File

@@ -1,8 +1,8 @@
import * as React from 'react';
import * as Sentry from '@sentry/react';
import { VsLabel } from '../VscodeComponents';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { VSCodeLabel } from '../VSCode';
export interface IFieldBoundaryProps {
fieldName: string;
@@ -34,11 +34,11 @@ export default class FieldBoundary extends React.Component<
if (this.state.hasError) {
return (
<div className={`metadata_field`}>
<VsLabel>
<VSCodeLabel>
<div className={`metadata_field__label`}>
<span style={{ lineHeight: '16px' }}>{this.props.fieldName}</span>
</div>
</VsLabel>
</VSCodeLabel>
<div className={`metadata_field__error`}>
<span>
{l10n.t(LocalizationKey.panelErrorBoundaryFieldBoundaryLabel)}

View File

@@ -11,7 +11,7 @@ import { LocalizationKey } from '../../../localization';
export interface IDateTimeFieldProps extends BaseFieldProps<Date | null> {
format?: string;
onChange: (date: Date) => void;
onChange: (date: string) => void;
}
type InputProps = JSX.IntrinsicElements['input'];
@@ -37,7 +37,11 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({
const onDateChange = React.useCallback((date: Date) => {
setDateValue(date);
onChange(date);
if (format) {
onChange(DateHelper.format(date, format) || "");
} else {
onChange(date.toISOString());
}
}, [format, onChange]);
const showRequiredState = useMemo(() => {

View File

@@ -0,0 +1,71 @@
import * as React from 'react';
import { BlockFieldData, Field, PanelSettings } from '../../../models';
import { IMetadata } from '../Metadata';
import { FieldTitle } from './FieldTitle';
export interface IFieldCollectionProps {
field: Field;
parent: IMetadata;
parentFields: string[];
blockData: BlockFieldData | undefined;
settings: PanelSettings;
renderFields: (
ctFields: Field[],
parent: IMetadata,
parentFields: string[],
blockData?: BlockFieldData,
onFieldUpdate?: (field: string | undefined, value: any, parents: string[]) => void,
parentBlock?: string | null
) => (JSX.Element | null)[] | undefined;
onChange: (field: string | undefined, value: any, parents: string[]) => void;
}
export const FieldCollection: React.FunctionComponent<IFieldCollectionProps> = ({
field,
parent,
parentFields,
blockData,
settings,
renderFields,
onChange
}: React.PropsWithChildren<IFieldCollectionProps>) => {
const [fields, setFields] = React.useState<Field[]>([]);
React.useEffect(() => {
if (!settings.fieldGroups) {
return
}
const group = settings.fieldGroups.find((group) => group.id === field.fieldGroup);
if (group) {
setFields(group.fields);
}
}, [field, settings?.fieldGroups]);
if (!fields || fields.length === 0) {
return null;
}
return (
<div className={`metadata_field__box`}>
<FieldTitle
className={`metadata_field__label_parent`}
label={field.title || field.name}
icon={undefined}
required={field.required}
/>
{field.description && (
<p className={`metadata_field__description`}>{field.description}</p>
)}
{renderFields(
fields,
parent,
[...parentFields, field.name],
blockData,
onChange
)}
</div>
);
};

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { useMemo } from 'react';
import { VsLabel } from '../VscodeComponents';
import { RequiredAsterix } from './RequiredAsterix';
import { VSCodeLabel } from '../VSCode';
export interface IFieldTitleProps {
label: string | JSX.Element;
@@ -23,7 +23,7 @@ export const FieldTitle: React.FunctionComponent<IFieldTitleProps> = ({
}, [icon]);
return (
<VsLabel>
<VSCodeLabel>
<div className={`metadata_field__label ${className || ''}`}>
<div>
{Icon}
@@ -33,6 +33,6 @@ export const FieldTitle: React.FunctionComponent<IFieldTitleProps> = ({
{actionElement}
</div>
</VsLabel>
</VSCodeLabel>
);
};

View File

@@ -26,7 +26,8 @@ import {
PreviewImageField,
PreviewImageValue,
NumberField,
CustomField
CustomField,
FieldCollection
} from '.';
import { fieldWhenClause } from '../../../utils/fieldWhenClause';
import { ContentTypeRelationshipField } from './ContentTypeRelationshipField';
@@ -521,6 +522,27 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
} else {
return null;
}
} else if (field.type === "fieldCollection") {
if (!parent[field.name]) {
parent[field.name] = {};
}
const subMetadata = parent[field.name] as IMetadata;
return (
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<FieldCollection
key={field.name}
field={field}
parent={subMetadata}
parentFields={parentFields}
renderFields={renderFields}
settings={settings}
blockData={blockData}
onChange={onSendUpdate}
/>
</FieldBoundary>
);
} else {
console.warn(l10n.t(LocalizationKey.panelFieldsWrapperFieldUnknown, field.type));
return null;

View File

@@ -1,9 +1,11 @@
export * from './ChoiceButton';
export * from './ChoiceField';
export * from './ContentTypeRelationshipField';
export * from './CustomField';
export * from './DataFileField';
export * from './DateTimeField';
export * from './DraftField';
export * from './FieldCollection';
export * from './FieldMessage';
export * from './FieldTitle';
export * from './FileField';

View File

@@ -1,9 +1,9 @@
import * as React from 'react';
import { FileInfo } from '../../models';
import { FileItem } from './FileItem';
import { VsLabel } from './VscodeComponents';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { VSCodeLabel } from './VSCode';
export interface IFileListProps {
folderName: string;
@@ -22,9 +22,9 @@ const FileList: React.FunctionComponent<IFileListProps> = ({
return (
<div className={`file_list`}>
<VsLabel>
<VSCodeLabel>
{folderName} - {files.length === 1 ? l10n.t(LocalizationKey.panelFileListLabelSingular) : l10n.t(LocalizationKey.panelFileListLabelPlural)}: {totalFiles}
</VsLabel>
</VSCodeLabel>
<ul className="file_list__items">
{files &&

View File

@@ -2,9 +2,9 @@ import * as React from 'react';
import { FolderInfo } from '../../models';
import { Collapsible } from './Collapsible';
import { FileList } from './FileList';
import { VsLabel } from './VscodeComponents';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { VSCodeLabel } from './VSCode';
export interface IFolderAndFilesProps {
data: FolderInfo[] | undefined;
@@ -29,15 +29,15 @@ const FolderAndFiles: React.FunctionComponent<IFolderAndFilesProps> = ({
{folder.lastModified ? (
<div key={`${folder.title}-${idx}`}>
<FileList
folderName={folder.title}
folderName={folder.locale ? `${folder.title} (${folder.localeTitle || folder.locale})` : folder.title}
totalFiles={folder.files}
files={folder.lastModified}
/>
</div>
) : isBase ? (
<VsLabel key={`${folder.title}-${idx}`}>
<VSCodeLabel key={`${folder.title}-${idx}`}>
{folder.title}: {folder.files} {folder.files > 1 ? l10n.t(LocalizationKey.panelFileListLabelPlural) : l10n.t(LocalizationKey.panelFileListLabelSingular)}
</VsLabel>
</VSCodeLabel>
) : null}
</div>
))}

View File

@@ -3,12 +3,12 @@ import { PanelSettings } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { useDebounce } from '../../hooks/useDebounce';
import { Collapsible } from './Collapsible';
import { VsLabel } from './VscodeComponents';
import useStartCommand from '../hooks/useStartCommand';
import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react';
import { Messenger } from '@estruyf/vscode/dist/client';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { VSCodeLabel } from './VSCode';
export interface IGlobalSettingsProps {
settings: PanelSettings | undefined;
@@ -78,25 +78,25 @@ const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({
title={l10n.t(LocalizationKey.panelGlobalSettingsTitle)}
>
<div className={`base__action`}>
<VsLabel>
<VSCodeLabel>
{l10n.t(LocalizationKey.panelGlobalSettingsActionModifiedDateLabel)}
</VsLabel>
</VSCodeLabel>
<VSCodeCheckbox checked={modifiedDateUpdate} onClick={onDateCheck}>
{l10n.t(LocalizationKey.panelGlobalSettingsActionModifiedDateDescription)}
</VSCodeCheckbox>
</div>
<div className={`base__action`}>
<VsLabel>
<VSCodeLabel>
{l10n.t(LocalizationKey.panelGlobalSettingsActionFrontMatterLabel)}
</VsLabel>
</VSCodeLabel>
<VSCodeCheckbox checked={fmHighlighting} onClick={onHighlightCheck}>
{l10n.t(LocalizationKey.panelGlobalSettingsActionFrontMatterDescription)}
</VSCodeCheckbox>
</div>
<div className={`base__action`}>
<VsLabel>
<VSCodeLabel>
{l10n.t(LocalizationKey.panelGlobalSettingsActionPreviewLabel)}
</VsLabel>
</VSCodeLabel>
<input
type={`text`}
placeholder={l10n.t(LocalizationKey.dashboardPreviewInputPlaceholder, `http://localhost:1313`)}
@@ -105,9 +105,9 @@ const GlobalSettings: React.FunctionComponent<IGlobalSettingsProps> = ({
/>
</div>
<div className={`base__action`}>
<VsLabel>
<VSCodeLabel>
{l10n.t(LocalizationKey.panelGlobalSettingsActionServerLabel)}
</VsLabel>
</VSCodeLabel>
<input
type={`text`}
placeholder={l10n.t(LocalizationKey.panelGlobalSettingsActionServerPlaceholder, `hugo server -D`)}

View File

@@ -1,23 +0,0 @@
import * as React from 'react';
export interface ICheckIconProps {}
export const CheckIcon: React.FunctionComponent<ICheckIconProps> = (
props: React.PropsWithChildren<ICheckIconProps>
) => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.431 3.323l-8.47 10-.79-.036-3.35-4.77.818-.574 2.978 4.24 8.051-9.506.764.646z"
/>
</svg>
);
};

View File

@@ -1,23 +0,0 @@
import * as React from 'react';
export interface IWarningIconProps {}
export const WarningIcon: React.FunctionComponent<IWarningIconProps> = (
props: React.PropsWithChildren<IWarningIconProps>
) => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.56 1h.88l6.54 12.26-.44.74H1.44L1 13.26 7.56 1zM8 2.28L2.28 13H13.7L8 2.28zM8.625 12v-1h-1.25v1h1.25zm-1.25-2V6h1.25v4h-1.25z"
/>
</svg>
);
};

Some files were not shown because too many files have changed in this diff Show More