mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
Compare commits
43 Commits
azure-tran
...
issue/671
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ae7cb27ce | ||
|
|
5e77419f5a | ||
|
|
ec9f55b982 | ||
|
|
fdcfdc971d | ||
|
|
2bc103026b | ||
|
|
23b1efec55 | ||
|
|
2a8d7b0ebe | ||
|
|
3b26944a4a | ||
|
|
78cac94dd6 | ||
|
|
9c6845ed8a | ||
|
|
7633ac91be | ||
|
|
282527c90d | ||
|
|
07fbf8bdb9 | ||
|
|
2e35da3d91 | ||
|
|
2bd607b13c | ||
|
|
106f1e6c94 | ||
|
|
54cd3ead64 | ||
|
|
7e9bd5b0ce | ||
|
|
9086868817 | ||
|
|
4bff53299e | ||
|
|
ee101cfe4d | ||
|
|
247051f592 | ||
|
|
e6b6bba7df | ||
|
|
be5d15d2f8 | ||
|
|
65364b8486 | ||
|
|
6dd82bd4fe | ||
|
|
e0b18465dc | ||
|
|
661efcf23f | ||
|
|
152f36e352 | ||
|
|
08697abba4 | ||
|
|
0a530dce27 | ||
|
|
63e296d62f | ||
|
|
003d93b0f2 | ||
|
|
59528a3db0 | ||
|
|
c298f2fd69 | ||
|
|
b02a80c28e | ||
|
|
f19bd07359 | ||
|
|
83b9f2380e | ||
|
|
3f88b05a1c | ||
|
|
48ada1c352 | ||
|
|
a8777c4032 | ||
|
|
fe5df3779b | ||
|
|
91ec23e77c |
46
.github/actions/localization/action.yml
vendored
Normal file
46
.github/actions/localization/action.yml
vendored
Normal 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: .
|
||||
66
.github/workflows/release-beta.yml
vendored
66
.github/workflows/release-beta.yml
vendored
@@ -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 }}
|
||||
|
||||
66
.github/workflows/release.yml
vendored
66
.github/workflows/release.yml
vendored
@@ -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 }}
|
||||
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,6 +1,24 @@
|
||||
# Change Log
|
||||
|
||||
## [10.0.0] - 2024-xx-xx - [Release notes](https://beta.frontmatter.codes/updates/v10.0.0)
|
||||
## [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
|
||||
|
||||
@@ -18,6 +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
|
||||
- Support for using the `fieldCollection` field in a `block` field
|
||||
- Updated the list of commands which are available in the command palette
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
|
||||
@@ -193,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)
|
||||
|
||||
21
README.md
21
README.md
@@ -199,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
17
SUPPORT.md
Normal 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).
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -146,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",
|
||||
@@ -229,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",
|
||||
|
||||
@@ -698,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
87
package-lock.json
generated
@@ -1,19 +1,18 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "10.0.0",
|
||||
"version": "10.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "10.0.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"
|
||||
|
||||
399
package.json
399
package.json
@@ -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": "10.0.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",
|
||||
@@ -532,7 +540,8 @@
|
||||
"categories"
|
||||
],
|
||||
"markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%",
|
||||
"items": [{
|
||||
"items": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"contentFolders",
|
||||
@@ -605,7 +614,8 @@
|
||||
"command": {
|
||||
"$id": "#scriptCommand",
|
||||
"type": "string",
|
||||
"anyOf": [{
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"node",
|
||||
"bash",
|
||||
@@ -801,7 +811,8 @@
|
||||
"title",
|
||||
"file"
|
||||
],
|
||||
"anyOf": [{
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"schema"
|
||||
]
|
||||
@@ -855,7 +866,8 @@
|
||||
"id",
|
||||
"path"
|
||||
],
|
||||
"anyOf": [{
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"schema"
|
||||
]
|
||||
@@ -1096,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": {
|
||||
@@ -1345,7 +1360,8 @@
|
||||
"default": "",
|
||||
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%",
|
||||
"not": {
|
||||
"anyOf": [{
|
||||
"anyOf": [
|
||||
{
|
||||
"const": ""
|
||||
},
|
||||
{
|
||||
@@ -1539,7 +1555,8 @@
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"allOf": [{
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -1747,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": {
|
||||
@@ -1801,7 +1821,8 @@
|
||||
"type": "string",
|
||||
"description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%",
|
||||
"not": {
|
||||
"anyOf": [{
|
||||
"anyOf": [
|
||||
{
|
||||
"const": ""
|
||||
},
|
||||
{
|
||||
@@ -1986,7 +2007,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [{
|
||||
"commands": [
|
||||
{
|
||||
"command": "frontMatter.project.switch",
|
||||
"title": "%command.frontMatter.project.switch%",
|
||||
"category": "Front Matter",
|
||||
@@ -2312,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"
|
||||
@@ -2407,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"
|
||||
@@ -2427,7 +2457,8 @@
|
||||
"group": "frontmatter@3"
|
||||
}
|
||||
],
|
||||
"commandPalette": [{
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "frontMatter.init",
|
||||
"when": "frontMatterCanInit"
|
||||
},
|
||||
@@ -2435,14 +2466,6 @@
|
||||
"command": "frontMatter.project.switch",
|
||||
"when": "frontMatter:project:switch:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createTemplate",
|
||||
"when": "!frontMatterCanInit"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.preview",
|
||||
"when": "frontMatterCanOpenPreview"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard.data",
|
||||
"when": "frontMatter:dashboard:data:enabled"
|
||||
@@ -2459,10 +2482,26 @@
|
||||
"command": "frontMatter.i18n.create",
|
||||
"when": "frontMatter:i18n:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.authenticate",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.remap",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertTags",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertCategories",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.registerFolder",
|
||||
"when": "false"
|
||||
@@ -2523,10 +2562,38 @@
|
||||
"command": "frontMatter.markup.options",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.hyperlink",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.config.reload",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.initTemplate",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
@@ -2547,14 +2614,6 @@
|
||||
"command": "frontMatter.insertCategories",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertTags",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createTemplate",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.preview",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
@@ -2571,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"
|
||||
@@ -2612,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:*",
|
||||
@@ -2683,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",
|
||||
@@ -2708,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",
|
||||
@@ -2755,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",
|
||||
@@ -2786,4 +2847,4 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
@@ -365,25 +365,25 @@ export class Folders {
|
||||
} else if (i18n.locale !== folder.defaultLocale && i18n.path) {
|
||||
localeFolders.push({
|
||||
...folder,
|
||||
title: `${folder.title} (${i18n.title})`,
|
||||
title: folder.title,
|
||||
originalPath: folder.path,
|
||||
locale: i18n.locale,
|
||||
localeTitle: i18n?.title || i18n.locale,
|
||||
localeSourcePath: sourcePath,
|
||||
path: join(folderPath, i18n.path)
|
||||
path: parseWinPath(join(folderPath, i18n.path))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultTitle = defaultLocale?.title
|
||||
? `${folder.title} (${defaultLocale.title})`
|
||||
: folder.title;
|
||||
contentFolders.push({
|
||||
...folder,
|
||||
title: defaultTitle,
|
||||
title: folder.title,
|
||||
locale: folder.defaultLocale,
|
||||
localeTitle: defaultLocale?.title || folder.defaultLocale,
|
||||
originalPath: folder.path,
|
||||
localeSourcePath: sourcePath,
|
||||
path: join(folderPath, defaultLocale?.path || '')
|
||||
path: parseWinPath(join(folderPath, defaultLocale?.path || ''))
|
||||
});
|
||||
|
||||
contentFolders.push(...localeFolders);
|
||||
@@ -670,7 +670,9 @@ export class Folders {
|
||||
return {
|
||||
title: folder.title,
|
||||
files: files.length,
|
||||
lastModified: fileStats
|
||||
lastModified: fileStats,
|
||||
locale: folder.locale,
|
||||
localeTitle: folder.localeTitle
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ export class Settings {
|
||||
const taxonomy = type === TaxonomyType.Tag ? 'tag' : 'category';
|
||||
|
||||
const newOption = await vscode.window.showInputBox({
|
||||
prompt: l10n.t(LocalizationKey.commandsFoldersCreateInputPrompt, taxonomy),
|
||||
placeHolder: l10n.t(LocalizationKey.commandsFoldersCreateInputPlaceholder, taxonomy),
|
||||
prompt: l10n.t(LocalizationKey.commandsSettingsCreateInputPrompt, taxonomy),
|
||||
placeHolder: l10n.t(LocalizationKey.commandsSettingsCreateInputPlaceholder, taxonomy),
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -400,7 +424,8 @@ export class i18n {
|
||||
);
|
||||
|
||||
if (!translations || translations.length < 3) {
|
||||
throw new Error('Invalid response');
|
||||
resolve(article);
|
||||
return;
|
||||
}
|
||||
|
||||
article.data.title = article.data.title ? translations[0] : '';
|
||||
|
||||
@@ -21,6 +21,9 @@ export const GeneralCommands = {
|
||||
get: 'getSecret',
|
||||
set: 'setSecret'
|
||||
},
|
||||
content: {
|
||||
locales: 'getContentLocales'
|
||||
},
|
||||
runCommand: 'runCommand',
|
||||
getLocalization: 'getLocalization',
|
||||
openOnWebsite: 'openOnWebsite'
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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.`
|
||||
];
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
|
||||
37
src/dashboardWebView/components/Common/ItemSelection.tsx
Normal file
37
src/dashboardWebView/components/Common/ItemSelection.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
255
src/dashboardWebView/components/Header/ActionsBar.tsx
Normal file
255
src/dashboardWebView/components/Header/ActionsBar.tsx
Normal 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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
26
src/dashboardWebView/components/Header/ActionsBarItem.tsx
Normal file
26
src/dashboardWebView/components/Header/ActionsBarItem.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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`} />
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
60
src/dashboardWebView/components/Media/MediaItemPanel.tsx
Normal file
60
src/dashboardWebView/components/Media/MediaItemPanel.tsx
Normal 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;
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
17
src/dashboardWebView/hooks/useMediaFolder.tsx
Normal file
17
src/dashboardWebView/hooks/useMediaFolder.tsx
Normal 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
|
||||
};
|
||||
}
|
||||
91
src/dashboardWebView/hooks/useMediaInfo.tsx
Normal file
91
src/dashboardWebView/hooks/useMediaInfo.tsx
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -268,14 +268,9 @@ export default function usePages(pages: Page[]) {
|
||||
}
|
||||
|
||||
if (pages && pages.length > 0) {
|
||||
// Store the locale information
|
||||
const config: I18nConfig[] = [];
|
||||
pages.forEach((page) => {
|
||||
if (page.fmLocale && !config.some(locale => locale.locale === page.fmLocale?.locale)) {
|
||||
config.push(page.fmLocale);
|
||||
}
|
||||
messageHandler.request<I18nConfig[]>(GeneralCommands.toVSCode.content.locales).then((config) => {
|
||||
setLocales(config || []);
|
||||
});
|
||||
setLocales(config);
|
||||
}
|
||||
}, [settings?.draftField, pages, sorting, search, tag, category, locale, filters, folder]);
|
||||
|
||||
|
||||
20
src/dashboardWebView/hooks/useSelectedItems.tsx
Normal file
20
src/dashboardWebView/hooks/useSelectedItems.tsx
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
36
src/dashboardWebView/providers/FilesProvider.tsx
Normal file
36
src/dashboardWebView/providers/FilesProvider.tsx
Normal 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 };
|
||||
52
src/dashboardWebView/providers/I10nProvider.tsx
Normal file
52
src/dashboardWebView/providers/I10nProvider.tsx
Normal 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 };
|
||||
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const MultiSelectedItemsAtom = atom<string[]>({
|
||||
key: 'MultiSelectedItemsAtom',
|
||||
default: []
|
||||
});
|
||||
12
src/dashboardWebView/state/atom/SelectedItemActionAtom.ts
Normal file
12
src/dashboardWebView/state/atom/SelectedItemActionAtom.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const SelectedItemActionAtom = atom<
|
||||
| {
|
||||
path: string;
|
||||
action: 'view' | 'edit';
|
||||
}
|
||||
| undefined
|
||||
>({
|
||||
key: 'SelectedItemActionAtom',
|
||||
default: undefined
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -39,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';
|
||||
@@ -378,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;
|
||||
}
|
||||
@@ -386,7 +386,7 @@ export class ArticleHelper {
|
||||
const articleCt = ArticleHelper.getContentType(article);
|
||||
const modDateField = articleCt.fields.find((f) => f.isModifiedDate);
|
||||
|
||||
return modDateField?.name || DefaultFields.LastModified;
|
||||
return modDateField;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -656,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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -483,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
|
||||
*/
|
||||
@@ -739,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
|
||||
*/
|
||||
@@ -2296,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.
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface ContentFolder {
|
||||
extended?: boolean;
|
||||
|
||||
locale?: string;
|
||||
localeTitle?: string;
|
||||
localeSourcePath?: string;
|
||||
defaultLocale?: string;
|
||||
locales: I18nConfig[];
|
||||
|
||||
@@ -191,6 +191,8 @@ export interface FolderInfo {
|
||||
title: string;
|
||||
files: number;
|
||||
lastModified: FileInfo[];
|
||||
locale?: string;
|
||||
localeTitle?: string;
|
||||
}
|
||||
|
||||
export interface FileInfo extends FileStat {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>))}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
71
src/panelWebView/components/Fields/FieldCollection.tsx
Normal file
71
src/panelWebView/components/Fields/FieldCollection.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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`)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -2,10 +2,10 @@ import * as React from 'react';
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { Field, PanelSettings } from '../../../models';
|
||||
import { PencilIcon } from '@heroicons/react/24/outline';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { JsonFieldRecords, JsonFieldForm, JsonFieldSelector } from '.';
|
||||
import { SortEnd } from 'react-sortable-hoc';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import { VSCodeLabel } from '../VSCode';
|
||||
|
||||
export interface IJsonFieldProps {
|
||||
label: string;
|
||||
@@ -101,12 +101,12 @@ export const JsonField: React.FunctionComponent<IJsonFieldProps> = ({
|
||||
|
||||
return (
|
||||
<div className="json_data__field">
|
||||
<VsLabel>
|
||||
<VSCodeLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<PencilIcon style={{ width: '16px', height: '16px' }} />{' '}
|
||||
<span style={{ lineHeight: '16px' }}>{label}</span>
|
||||
</div>
|
||||
</VsLabel>
|
||||
</VSCodeLabel>
|
||||
|
||||
<JsonFieldSelector
|
||||
field={field}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CircleStackIcon, PlusIcon } from '@heroicons/react/24/outline';
|
||||
import * as React from 'react';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { JsonFieldRecord } from '.';
|
||||
import { SortableContainer, SortEnd } from 'react-sortable-hoc';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { VSCodeLabel } from '../VSCode';
|
||||
|
||||
export interface IJsonFieldRecordsProps {
|
||||
records: any[];
|
||||
@@ -33,7 +33,7 @@ export const JsonFieldRecords = ({
|
||||
|
||||
return (
|
||||
<div className="json_data__list">
|
||||
<VsLabel>
|
||||
<VSCodeLabel>
|
||||
<div className={`metadata_field__label`}>
|
||||
<div>
|
||||
<CircleStackIcon style={{ width: '16px', height: '16px' }} />
|
||||
@@ -44,7 +44,7 @@ export const JsonFieldRecords = ({
|
||||
<PlusIcon style={{ width: '16px', height: '16px' }} />
|
||||
</button>
|
||||
</div>
|
||||
</VsLabel>
|
||||
</VSCodeLabel>
|
||||
|
||||
<Container onSortEnd={onSort} useDragHandle>
|
||||
{records.map((v: any, idx: number) => (
|
||||
|
||||
@@ -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 ISeoDetailsProps {
|
||||
allowedLength: number;
|
||||
@@ -23,30 +16,33 @@ const SeoDetails: React.FunctionComponent<ISeoDetailsProps> = (
|
||||
) => {
|
||||
const { allowedLength, title, value, valueTitle, noValidation } = props;
|
||||
|
||||
const validate = () => {
|
||||
const validate = React.useMemo(() => {
|
||||
if (noValidation) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value <= allowedLength ? 'valid' : 'not-valid';
|
||||
};
|
||||
}, [value, allowedLength, noValidation]);
|
||||
|
||||
return (
|
||||
<div className={`seo__status__details ${validate()}`}>
|
||||
<div className={`seo__status__details ${validate}`}>
|
||||
<h4>{title}</h4>
|
||||
|
||||
<VsTable bordered>
|
||||
<VsTableHeader slot="header">
|
||||
<VsTableHeaderCell className={validate()}>{valueTitle}</VsTableHeaderCell>
|
||||
<VsTableHeaderCell>{l10n.t(LocalizationKey.panelSeoDetailsRecommended)}</VsTableHeaderCell>
|
||||
</VsTableHeader>
|
||||
<VsTableBody slot="body">
|
||||
<VsTableRow>
|
||||
<VsTableCell className={validate()}>{value}</VsTableCell>
|
||||
<VsTableCell>{allowedLength}</VsTableCell>
|
||||
</VsTableRow>
|
||||
</VsTableBody>
|
||||
</VsTable>
|
||||
<VSCodeTable>
|
||||
<VSCodeTableHeader>
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableHead className={validate}>{valueTitle}</VSCodeTableHead>
|
||||
<VSCodeTableHead>{l10n.t(LocalizationKey.panelSeoDetailsRecommended)}</VSCodeTableHead>
|
||||
</VSCodeTableRow>
|
||||
</VSCodeTableHeader>
|
||||
|
||||
<VSCodeTableBody>
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell className={validate}>{value}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{allowedLength}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
</VSCodeTableBody>
|
||||
</VSCodeTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { ValidInfo } from './ValidInfo';
|
||||
import { VsTableCell, VsTableRow } from './VscodeComponents';
|
||||
import { VSCodeTableCell, VSCodeTableRow } from './VSCode/VSCodeTable';
|
||||
|
||||
export interface ISeoFieldInfoProps {
|
||||
title: string;
|
||||
@@ -16,15 +16,11 @@ const SeoFieldInfo: React.FunctionComponent<ISeoFieldInfoProps> = ({
|
||||
isValid
|
||||
}: React.PropsWithChildren<ISeoFieldInfoProps>) => {
|
||||
return (
|
||||
<VsTableRow>
|
||||
<VsTableCell className={`table__cell table__title`}>{title}</VsTableCell>
|
||||
<VsTableCell className={`table__cell`}>
|
||||
{value}/{recommendation}
|
||||
</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation`}>
|
||||
{isValid !== undefined ? <ValidInfo label={undefined} isValid={isValid} /> : <span>-</span>}
|
||||
</VsTableCell>
|
||||
</VsTableRow>
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell className={`capitalize`}>{title}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{value}/{recommendation}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{isValid !== undefined ? <ValidInfo label={undefined} isValid={isValid} /> : <span>-</span>}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { ValidInfo } from './ValidInfo';
|
||||
import { VsTableCell, VsTableRow } from './VscodeComponents';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../localization';
|
||||
import { VSCodeTableCell, VSCodeTableRow } from './VSCode/VSCodeTable';
|
||||
|
||||
export interface ISeoKeywordInfoProps {
|
||||
keyword: string;
|
||||
@@ -71,22 +71,22 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<VsTableRow>
|
||||
<VsTableCell className={`table__cell`}>{keyword}</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation table__cell__seo_details`}>
|
||||
<div>
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell>{keyword}</VSCodeTableCell>
|
||||
<VSCodeTableCell className={` table__cell__validation`}>
|
||||
<div className='flex items-center'>
|
||||
<ValidInfo
|
||||
label={l10n.t(LocalizationKey.commonTitle)}
|
||||
isValid={!!title && title.toLowerCase().includes(keyword.toLowerCase())}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className='flex items-center'>
|
||||
<ValidInfo
|
||||
label={l10n.t(LocalizationKey.commonDescription)}
|
||||
isValid={!!description && description.toLowerCase().includes(keyword.toLowerCase())}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className='flex items-center'>
|
||||
<ValidInfo
|
||||
label={l10n.t(LocalizationKey.commonSlug)}
|
||||
isValid={
|
||||
@@ -96,16 +96,18 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className='flex items-center'>
|
||||
<ValidInfo
|
||||
label={l10n.t(LocalizationKey.panelSeoKeywordInfoValidInfoContent)}
|
||||
isValid={!!content && content.toLowerCase().includes(keyword.toLowerCase())}
|
||||
/>
|
||||
</div>
|
||||
{headings && headings.length > 0 && <div>{checkHeadings()}</div>}
|
||||
{wordCount && <div>{density()}</div>}
|
||||
</VsTableCell>
|
||||
</VsTableRow>
|
||||
{headings && headings.length > 0 &&
|
||||
<div className='flex items-center'>{checkHeadings()}</div>}
|
||||
{wordCount &&
|
||||
<div className='flex items-center'>{density()}</div>}
|
||||
</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { SeoKeywordInfo } from './SeoKeywordInfo';
|
||||
import { VsTable, VsTableBody, VsTableHeader, VsTableHeaderCell } from './VscodeComponents';
|
||||
import { ErrorBoundary } from '@sentry/react';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../localization';
|
||||
import { VSCodeTable, VSCodeTableBody, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable';
|
||||
|
||||
export interface ISeoKeywordsProps {
|
||||
keywords: string[] | null;
|
||||
@@ -54,16 +54,19 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
|
||||
<div className={`seo__status__keywords`}>
|
||||
<h4>{l10n.t(LocalizationKey.panelSeoKeywordsTitle)}</h4>
|
||||
|
||||
<VsTable bordered columns={['30%', 'auto']}>
|
||||
<VsTableHeader slot="header">
|
||||
<VsTableHeaderCell className={`table__cell`}>
|
||||
{l10n.t(LocalizationKey.panelSeoKeywordsHeaderKeyword)}
|
||||
</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>
|
||||
{l10n.t(LocalizationKey.panelSeoKeywordsHeaderDetails)}
|
||||
</VsTableHeaderCell>
|
||||
</VsTableHeader>
|
||||
<VsTableBody slot="body">
|
||||
<VSCodeTable>
|
||||
<VSCodeTableHeader>
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableHead>
|
||||
{l10n.t(LocalizationKey.panelSeoKeywordsHeaderKeyword)}
|
||||
</VSCodeTableHead>
|
||||
<VSCodeTableHead>
|
||||
{l10n.t(LocalizationKey.panelSeoKeywordsHeaderDetails)}
|
||||
</VSCodeTableHead>
|
||||
</VSCodeTableRow>
|
||||
</VSCodeTableHeader>
|
||||
|
||||
<VSCodeTableBody>
|
||||
{validateKeywords().map((keyword, index) => {
|
||||
return (
|
||||
<ErrorBoundary key={keyword} fallback={<div />}>
|
||||
@@ -71,11 +74,11 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
|
||||
</ErrorBoundary>
|
||||
);
|
||||
})}
|
||||
</VsTableBody>
|
||||
</VsTable>
|
||||
</VSCodeTableBody>
|
||||
</VSCodeTable>
|
||||
|
||||
{data.wordCount && (
|
||||
<div className={`seo__status__note`}>
|
||||
<div className={`text-xs mt-2`}>
|
||||
{l10n.t(LocalizationKey.panelSeoKeywordsDensity)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { SEO } from '../../models/PanelSettings';
|
||||
import { TagType } from '../TagType';
|
||||
import { ArticleDetails } from './ArticleDetails';
|
||||
@@ -9,9 +8,9 @@ import { SymbolKeywordIcon } from './Icons/SymbolKeywordIcon';
|
||||
import { SeoFieldInfo } from './SeoFieldInfo';
|
||||
import { SeoKeywords } from './SeoKeywords';
|
||||
import { TagPicker } from './TagPicker';
|
||||
import { VsTable, VsTableBody, VsTableHeader, VsTableHeaderCell } from './VscodeComponents';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../localization';
|
||||
import { VSCodeTable, VSCodeTableBody, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable';
|
||||
|
||||
export interface ISeoStatusProps {
|
||||
seo: SEO;
|
||||
@@ -27,90 +26,61 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
|
||||
unsetFocus
|
||||
}: React.PropsWithChildren<ISeoStatusProps>) => {
|
||||
const { title, slug } = data;
|
||||
const [isOpen, setIsOpen] = React.useState(true);
|
||||
const tableRef = React.useRef<HTMLElement>();
|
||||
const pushUpdate = React.useRef((value: boolean) => {
|
||||
setTimeout(() => {
|
||||
setIsOpen(value);
|
||||
}, 10);
|
||||
}).current;
|
||||
|
||||
const { descriptionField, titleField } = seo;
|
||||
|
||||
// Workaround for lit components not updating render
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
let height = 0;
|
||||
|
||||
tableRef.current?.childNodes.forEach((elm: any) => {
|
||||
height += elm.clientHeight;
|
||||
});
|
||||
|
||||
if (height > 0 && tableRef.current) {
|
||||
tableRef.current.style.height = `${height}px`;
|
||||
}
|
||||
}, 10);
|
||||
}, [title, data[titleField], data[descriptionField], data?.articleDetails?.wordCount]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tableContent = React.useMemo(() => {
|
||||
return (
|
||||
<div>
|
||||
<div className={`seo__status__details`}>
|
||||
<h4>{l10n.t(LocalizationKey.panelSeoStatusTitle)}</h4>
|
||||
|
||||
<VsTable ref={tableRef} bordered zebra>
|
||||
<VsTableHeader slot="header">
|
||||
<VsTableHeaderCell className={`table__cell`}>
|
||||
{l10n.t(LocalizationKey.panelSeoStatusHeaderProperty)}
|
||||
</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>
|
||||
{l10n.t(LocalizationKey.panelSeoStatusHeaderLength)}
|
||||
</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>
|
||||
{l10n.t(LocalizationKey.panelSeoStatusHeaderValid)}
|
||||
</VsTableHeaderCell>
|
||||
</VsTableHeader>
|
||||
<VsTableBody slot="body">
|
||||
{data[titleField] && seo.title > 0 && (
|
||||
<VSCodeTable>
|
||||
<VSCodeTableHeader>
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableHead>{l10n.t(LocalizationKey.panelSeoStatusHeaderProperty)}</VSCodeTableHead>
|
||||
<VSCodeTableHead>{l10n.t(LocalizationKey.panelSeoStatusHeaderLength)}</VSCodeTableHead>
|
||||
<VSCodeTableHead>{l10n.t(LocalizationKey.panelSeoStatusHeaderValid)}</VSCodeTableHead>
|
||||
</VSCodeTableRow>
|
||||
</VSCodeTableHeader>
|
||||
|
||||
<VSCodeTableBody>
|
||||
{data[titleField] && seo.title > 0 ? (
|
||||
<SeoFieldInfo
|
||||
title={titleField}
|
||||
value={data[titleField].length}
|
||||
recommendation={l10n.t(LocalizationKey.panelSeoStatusSeoFieldInfoCharacters, seo.title)}
|
||||
isValid={data[titleField].length <= seo.title}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{slug && seo.slug > 0 && (
|
||||
{slug && seo.slug > 0 ? (
|
||||
<SeoFieldInfo
|
||||
title={`slug`}
|
||||
value={slug.length}
|
||||
recommendation={l10n.t(LocalizationKey.panelSeoStatusSeoFieldInfoCharacters, seo.slug)}
|
||||
isValid={slug.length <= seo.slug}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{data[descriptionField] && seo.description > 0 && (
|
||||
{data[descriptionField] && seo.description > 0 ? (
|
||||
<SeoFieldInfo
|
||||
title={descriptionField}
|
||||
value={data[descriptionField].length}
|
||||
recommendation={l10n.t(LocalizationKey.panelSeoStatusSeoFieldInfoCharacters, seo.description)}
|
||||
isValid={data[descriptionField].length <= seo.description}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{seo.content > 0 && data?.articleDetails?.wordCount > 0 && (
|
||||
{seo.content > 0 && data?.articleDetails?.wordCount > 0 ? (
|
||||
<SeoFieldInfo
|
||||
title={l10n.t(LocalizationKey.panelSeoStatusSeoFieldInfoArticle)}
|
||||
value={data?.articleDetails?.wordCount}
|
||||
recommendation={l10n.t(LocalizationKey.panelSeoStatusSeoFieldInfoWords, seo.content)}
|
||||
/>
|
||||
)}
|
||||
</VsTableBody>
|
||||
</VsTable>
|
||||
) : null}
|
||||
</VSCodeTableBody>
|
||||
</VSCodeTable>
|
||||
</div>
|
||||
|
||||
<SeoKeywords
|
||||
@@ -139,10 +109,10 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
|
||||
<ArticleDetails details={data.articleDetails} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}, [data, seo, focusElm, unsetFocus]);
|
||||
|
||||
return (
|
||||
<Collapsible id={`seo`} title={l10n.t(LocalizationKey.panelSeoStatusCollapsibleTitle)} sendUpdate={pushUpdate}>
|
||||
<Collapsible id={`seo`} title={l10n.t(LocalizationKey.panelSeoStatusCollapsibleTitle)}>
|
||||
{!title && !data[descriptionField] ? (
|
||||
<div className={`seo__status__empty`}>
|
||||
<p>
|
||||
@@ -150,7 +120,7 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
renderContent()
|
||||
tableContent
|
||||
)}
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
27
src/panelWebView/components/VSCode/VSCodeLabel.tsx
Normal file
27
src/panelWebView/components/VSCode/VSCodeLabel.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IVSCodeLabelProps { }
|
||||
|
||||
export const VSCodeLabel: React.FunctionComponent<IVSCodeLabelProps> = ({
|
||||
children
|
||||
}: React.PropsWithChildren<IVSCodeLabelProps>) => {
|
||||
const DEFAULT_LINE_HEIGHT = 16;
|
||||
const DEFAULT_FONT_SIZE = 13;
|
||||
|
||||
const INPUT_LINE_HEIGHT_RATIO = DEFAULT_LINE_HEIGHT / DEFAULT_FONT_SIZE;
|
||||
|
||||
return (
|
||||
<label style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontFamily: "var(--vscode-font-family)",
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
fontWeight: "600",
|
||||
lineHeight: INPUT_LINE_HEIGHT_RATIO,
|
||||
cursor: "default",
|
||||
display: "block",
|
||||
padding: "5px 0"
|
||||
}}>
|
||||
{children}
|
||||
</label >
|
||||
);
|
||||
};
|
||||
103
src/panelWebView/components/VSCode/VSCodeTable.tsx
Normal file
103
src/panelWebView/components/VSCode/VSCodeTable.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "../../../utils/cn"
|
||||
|
||||
const VSCodeTable = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full text-base border-collapse indent-0 [&_tr:nth-child(2n)]:bg-[var(--vscode-keybindingTable-rowsBackground)]", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
VSCodeTable.displayName = "VSCodeTable"
|
||||
|
||||
const VSCodeTableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b bg-[var(--vscode-keybindingTable-headerBackground)]", className)} {...props} />
|
||||
))
|
||||
VSCodeTableHeader.displayName = "VSCodeTableHeader"
|
||||
|
||||
const VSCodeTableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
VSCodeTableBody.displayName = "VSCodeTableBody"
|
||||
|
||||
const VSCodeTableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
VSCodeTableFooter.displayName = "VSCodeTableFooter"
|
||||
|
||||
const VSCodeTableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-solid border-0 border-b border-b-[var(--vscode-editorGroup-border)] transition-colors [&_td]:border-r [&_td]:border-r-[var(--vscode-editorGroup-border)] [&_td:last-child]:border-r-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
VSCodeTableRow.displayName = "VSCodeTableRow"
|
||||
|
||||
const VSCodeTableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-6 px-2 py-2 text-left align-middle font-bold",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
VSCodeTableHead.displayName = "VSCodeTableHead"
|
||||
|
||||
const VSCodeTableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("border-solid border-0 h-6 px-2 overflow-hidden align-middle", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
VSCodeTableCell.displayName = "VSCodeTableCell"
|
||||
|
||||
export {
|
||||
VSCodeTable,
|
||||
VSCodeTableHeader,
|
||||
VSCodeTableBody,
|
||||
VSCodeTableFooter,
|
||||
VSCodeTableHead,
|
||||
VSCodeTableRow,
|
||||
VSCodeTableCell,
|
||||
}
|
||||
1
src/panelWebView/components/VSCode/index.ts
Normal file
1
src/panelWebView/components/VSCode/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './VSCodeLabel';
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { CheckIcon } from './Icons/CheckIcon';
|
||||
import { WarningIcon } from './Icons/WarningIcon';
|
||||
import { CheckIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export interface IValidInfoProps {
|
||||
label?: string;
|
||||
@@ -14,13 +13,9 @@ const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({
|
||||
return (
|
||||
<>
|
||||
{isValid ? (
|
||||
<span className="valid">
|
||||
<CheckIcon />
|
||||
</span>
|
||||
<CheckIcon className={`h-4 w-4 text-[#46ec86] mr-2`} />
|
||||
) : (
|
||||
<span className="warning">
|
||||
<WarningIcon />
|
||||
</span>
|
||||
<ExclamationTriangleIcon className={`h-4 w-4 text-[var(--vscode-statusBarItem-warningBackground)] mr-2`} />
|
||||
)}
|
||||
{label && <span>{label}</span>}
|
||||
</>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { wrapWc } from 'wc-react';
|
||||
|
||||
// @bendera/vscode-webview-elements
|
||||
export const VsTable = wrapWc(`vscode-table`);
|
||||
export const VsTableHeader = wrapWc(`vscode-table-header`);
|
||||
export const VsTableHeaderCell = wrapWc(`vscode-table-header-cell`);
|
||||
export const VsTableBody = wrapWc(`vscode-table-body`);
|
||||
export const VsTableRow = wrapWc(`vscode-table-row`);
|
||||
export const VsTableCell = wrapWc(`vscode-table-cell`);
|
||||
export const VsCollapsible = wrapWc(`vscode-collapsible`);
|
||||
export const VsLabel = wrapWc(`vscode-label`);
|
||||
// export const VsLabel = wrapWc(`vscode-label`);
|
||||
|
||||
@@ -10,13 +10,11 @@ import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { PanelSettingsAtom } from '../state';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
|
||||
export default function useMessages() {
|
||||
const [metadata, setMetadata] = useState<any>({});
|
||||
const [settings, setSettings] = useRecoilState(PanelSettingsAtom);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [localeReady, setLocaleReady] = useState<boolean>(false);
|
||||
const [focusElm, setFocus] = useState<TagType | null>(null);
|
||||
const [folderAndFiles, setFolderAndFiles] = useState<FolderInfo[] | undefined>(undefined);
|
||||
const [mediaSelecting, setMediaSelecting] = useState<DashboardData | undefined>(undefined);
|
||||
@@ -52,12 +50,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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,7 +91,6 @@ export default function useMessages() {
|
||||
loading,
|
||||
mediaSelecting,
|
||||
mode,
|
||||
localeReady,
|
||||
unsetFocus
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user