Compare commits

...

155 Commits

Author SHA1 Message Date
Elio Struyf
a571b34724 Merge pull request #795 from estruyf/dev
v10.1.0 release
2024-04-11 17:20:16 +02:00
Elio Struyf
f46e4999a1 Update changelog 2024-04-11 17:17:09 +02:00
Elio Struyf
f9138cb3c3 Release time 2024-04-11 17:13:36 +02:00
Elio Struyf
893c46362e revert change 2024-04-10 17:53:27 +02:00
Elio Struyf
9136841b30 #716 - Missing class 2024-04-08 14:01:04 +02:00
Elio Struyf
0e21093f92 Move taxonomy picker 2024-04-08 13:45:16 +02:00
Elio Struyf
3abd9589f1 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2024-04-04 16:57:47 +02:00
Elio Struyf
81265e3c49 #671 - Fix on metadata update 2024-04-04 16:57:40 +02:00
Elio Struyf
f6fd57e126 Merge pull request #790 from mayumih387/dev
Added and updated Japanese translations
2024-04-04 10:24:57 +02:00
mayumih387
20d613452f Added and updated Japanese translations 2024-04-04 15:13:18 +09:00
Elio Struyf
35a6c8bada Tab colors 2024-04-01 17:06:48 +02:00
Elio Struyf
0b7f58d0ab #785 - Added custom script actions to the bottom of the cards 2024-04-01 16:39:05 +02:00
Elio Struyf
c859874470 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2024-04-01 15:11:17 +02:00
Elio Struyf
d70d2284b4 #787 - Support for glob patterns in the page folder paths 2024-04-01 15:11:07 +02:00
Elio Struyf
03236da793 Small design tweaks 2024-03-30 12:41:07 +01:00
Elio Struyf
07935aec73 Update changelog 2024-03-30 10:33:29 +01:00
Elio Struyf
f64c8c5958 #786 - Remove on startup as VSCode now triggers on known commands 2024-03-30 10:33:10 +01:00
Elio Struyf
c4267a69fa #785 - Media actions 2024-03-29 17:34:28 +01:00
Elio Struyf
34b331b0ee #785 - Added content actions at the bottom of the card 2024-03-29 14:10:46 +01:00
Elio Struyf
5d0fc4f605 Update localization 2024-03-28 15:19:26 +01:00
Elio Struyf
169f4ef14a Fix localization 2024-03-28 15:19:05 +01:00
Elio Struyf
7ea386328c Added keybinding to refresh dashboard 2024-03-27 12:12:57 +01:00
Elio Struyf
c17400ce6d #783 - Always show custom panel views 2024-03-27 10:57:30 +01:00
Elio Struyf
7b20d9f23d #782 - Setting the correct view 2024-03-26 09:54:34 +01:00
Elio Struyf
449bb110c2 Small refactoring 2024-03-22 09:13:00 +01:00
Elio Struyf
0d3a99abe6 #777 - Fix for untitled files 2024-03-19 13:01:29 +01:00
Elio Struyf
d2b9307a65 #777 - Extra states for invalid files 2024-03-19 12:34:00 +01:00
Elio Struyf
3842777f71 #778 - Open file or webpage 2024-03-19 12:24:08 +01:00
Elio Struyf
3a74c14ba6 Update localization 2024-03-19 10:06:07 +01:00
Elio Struyf
a5ac7379bc #777 - Show error when front matter parsing failed 2024-03-18 14:31:29 +01:00
Elio Struyf
c245e1474c Icon updates 2024-03-15 14:48:15 +01:00
Elio Struyf
c82c081fce #773 - Rename files 2024-03-14 16:38:17 +01:00
Elio Struyf
31e344f358 Retry to check if external script is binded 2024-03-13 17:43:38 +01:00
Elio Struyf
366ae82318 Added count 2024-03-13 16:22:19 +01:00
Elio Struyf
c1a0609216 Update changelog 2024-03-13 15:02:23 +01:00
Elio Struyf
87bdabf515 Merge pull request #772 from estruyf/issue/671
#671 - Implement checkbox on media card
2024-03-13 15:00:46 +01:00
Elio Struyf
0ae7cb27ce Added localization 2024-03-13 14:58:58 +01:00
Elio Struyf
15870bcc99 #771 - Fix lowercased data label 2024-03-13 14:54:35 +01:00
Elio Struyf
5e77419f5a New table implementation 2024-03-13 14:52:06 +01:00
Elio Struyf
ec9f55b982 Localization actions 2024-03-13 11:32:42 +01:00
Elio Struyf
fdcfdc971d Multi-select hook 2024-03-12 20:40:05 +01:00
Elio Struyf
2bc103026b Editor panel + content action bar 2024-03-12 13:53:43 +01:00
Elio Struyf
e0cdc5cf65 Update sponsor 2024-03-11 17:17:56 +01:00
Elio Struyf
f39b707e30 Updated readme 2024-03-11 17:06:46 +01:00
Elio Struyf
23b1efec55 Multi-select actions 2024-03-11 16:52:14 +01:00
Elio Struyf
dd13d8779c Merge branch 'main' into dev 2024-03-01 10:47:38 +01:00
Elio Struyf
6f6015cf83 Merge branch 'main' of github.com:estruyf/vscode-front-matter 2024-03-01 10:33:18 +01:00
Elio Struyf
afd2878428 #769 - Fix settings on install 2024-03-01 10:33:13 +01:00
Elio Struyf
c66deb032c Merge pull request #770 from estruyf/patch-10.0.2
Patch 10.0.2
2024-03-01 10:05:04 +01:00
Elio Struyf
4c079b3e9d 10.0.2 2024-03-01 09:01:46 +01:00
Elio Struyf
03c2cd31d7 Update changelog 2024-03-01 09:01:38 +01:00
Elio Struyf
d1dba01923 #769 - Fix folder update 2024-03-01 09:00:42 +01:00
Elio Struyf
2a8d7b0ebe #671 - Implement checkbox on media card 2024-03-01 08:40:05 +01:00
Elio Struyf
3b26944a4a Update changelog 2024-02-29 08:40:01 +01:00
Elio Struyf
78cac94dd6 10.1.0 2024-02-29 08:38:46 +01:00
Elio Struyf
9c6845ed8a #768 - Update data view link 2024-02-29 08:38:31 +01:00
Elio Struyf
286ac4adfe Merge pull request #767 from estruyf/dev
#766 - Fix snippet placeholder retrieval
2024-02-28 21:13:56 +01:00
Elio Struyf
7633ac91be Update changelog 2024-02-28 21:12:17 +01:00
Elio Struyf
282527c90d 10.0.1 2024-02-28 21:11:27 +01:00
Elio Struyf
07fbf8bdb9 #766 - Fix snippet placeholder retrieval 2024-02-28 21:11:24 +01:00
Elio Struyf
6e2633572a Merge pull request #763 from estruyf/dev
Release v10.0.0
2024-02-28 16:34:35 +01:00
Elio Struyf
2e35da3d91 #746 - Fix in path placeholders on content creation 2024-02-28 15:54:11 +01:00
Elio Struyf
2bd607b13c Added a support.md file 2024-02-28 14:58:54 +01:00
Elio Struyf
106f1e6c94 Update script 2024-02-28 14:34:57 +01:00
Elio Struyf
54cd3ead64 Remove checkout 2024-02-28 14:32:07 +01:00
Elio Struyf
7e9bd5b0ce Add missing checkout 2024-02-28 14:29:35 +01:00
Elio Struyf
9086868817 Update action 2024-02-28 14:28:37 +01:00
Elio Struyf
4bff53299e Update action name 2024-02-28 14:26:41 +01:00
Elio Struyf
ee101cfe4d Update gh actions 2024-02-28 14:25:24 +01:00
Elio Struyf
247051f592 Updated GH action dependency 2024-02-28 14:10:44 +01:00
Elio Struyf
e6b6bba7df Updated beta release 2024-02-28 14:09:32 +01:00
Elio Struyf
be5d15d2f8 Updated changelog + date field default value 2024-02-28 11:43:36 +01:00
Elio Struyf
65364b8486 Update changelog for v10 release 2024-02-28 11:30:42 +01:00
Elio Struyf
6dd82bd4fe Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2024-02-27 09:43:51 +01:00
Elio Struyf
e0b18465dc Updated command palette commands 2024-02-27 09:43:16 +01:00
Elio Struyf
661efcf23f Update commands 2024-02-26 20:40:37 +01:00
Elio Struyf
152f36e352 Merge branch 'i18n' of github.com:estruyf/vscode-front-matter into i18n 2024-02-26 20:18:44 +01:00
Elio Struyf
08697abba4 Update commands 2024-02-26 20:18:32 +01:00
Elio Struyf
0a530dce27 Update readme 2024-02-26 15:51:40 +01:00
Elio Struyf
63e296d62f Sentry updates 2024-02-26 15:49:14 +01:00
Elio Struyf
003d93b0f2 Added telemetry information 2024-02-26 15:13:08 +01:00
Elio Struyf
59528a3db0 #746 - Slug handling when none is defined 2024-02-24 14:10:14 +01:00
Elio Struyf
c298f2fd69 #760 - Windows file path fixes in multilingual 2024-02-24 13:46:26 +01:00
Elio Struyf
b02a80c28e #760 - Add locale to recently modified panel section 2024-02-24 13:14:45 +01:00
Elio Struyf
f19bd07359 Support for using the fieldCollection field in a block field 2024-02-23 19:13:34 +01:00
Elio Struyf
83b9f2380e Date format fix 2024-02-23 16:37:15 +01:00
Elio Struyf
3f88b05a1c i10n provider for generic config 2024-02-23 09:26:41 +01:00
Elio Struyf
48ada1c352 #760 - Return locales from settings 2024-02-23 08:58:43 +01:00
Elio Struyf
a8777c4032 #760 - Type filter fix 2024-02-23 08:39:43 +01:00
Elio Struyf
fe5df3779b Merge branch 'azure-translator' into dev 2024-02-22 15:28:36 +01:00
Elio Struyf
91ec23e77c HTML text type 2024-02-22 15:28:23 +01:00
Elio Struyf
1b0a99b8fb Azure translations 2024-02-22 15:25:05 +01:00
Elio Struyf
4a0c1a4059 Update changelog 2024-02-22 13:35:14 +01:00
Elio Struyf
40c722e380 Update changelog 2024-02-22 13:30:40 +01:00
Elio Struyf
41a5e9ab7a Remove deprecated settings 2024-02-22 13:09:36 +01:00
Elio Struyf
a641aabc2a #749 - Support in extended configs 2024-02-22 11:44:23 +01:00
Elio Struyf
bc3b2c403d Update default 2024-02-22 11:15:15 +01:00
Elio Struyf
b4d2e4ea8b #749 - update name 2024-02-22 11:14:30 +01:00
Elio Struyf
b1e87d4f57 #760 - Fix to keep locale filter 2024-02-22 10:29:51 +01:00
Elio Struyf
241e660694 #756: Support for creating translation from any page + change path logic 2024-02-22 09:52:59 +01:00
Elio Struyf
03bc7e72fd Update version 2024-02-21 16:57:53 +01:00
Elio Struyf
0aca8fed16 10.0.0 2024-02-21 16:50:55 +01:00
Elio Struyf
60b5d7d759 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2024-02-21 16:50:18 +01:00
Elio Struyf
e12db5ec74 Merge pull request #759 from estruyf/i18n
New i18n feature
2024-02-21 16:49:16 +01:00
Elio Struyf
f1d345ebc2 Update localization 2024-02-21 10:49:39 +01:00
Elio Struyf
e9af7e1793 #756 - Move deepl key to secret storage 2024-02-21 10:00:48 +01:00
Elio Struyf
49e7fe6377 #756 - Support the ability to use Deepl 2024-02-20 21:59:36 +01:00
Elio Struyf
cc375801c2 #756 - Faster translation checks 2024-02-19 20:33:19 +01:00
Elio Struyf
4a53a180a7 #756 - Page bundle support added 2024-02-19 18:17:08 +01:00
Elio Struyf
51ece235f8 #756 - Language filter + card actions + submenu 2024-02-19 16:04:41 +01:00
Elio Struyf
36ac891c00 #756 - Language dropdown 2024-02-18 21:20:07 +01:00
Elio Struyf
5f0fd29cca #756 - i18n content creation 2024-02-18 16:12:01 +01:00
Elio Struyf
0428e561a8 #756 - first steps 2024-02-16 17:23:08 +01:00
Elio Struyf
bcba947c1d Fix menu in welcome view 2024-02-16 11:33:38 +01:00
Elio Struyf
c2d3496152 Small fix in git 2024-02-15 13:49:57 +01:00
Elio Struyf
7f1dc88bd4 Updated localization 2024-02-15 13:47:13 +01:00
Elio Struyf
83f4711103 Merge pull request #758 from estruyf/shadcn
Use dropdown of shadcn
2024-02-15 13:34:35 +01:00
Elio Struyf
0a8723c544 New dropdown implementation 2024-02-15 13:00:45 +01:00
Elio Struyf
bdce486a24 Use dropdown of shadcn 2024-02-14 18:14:58 +01:00
Elio Struyf
6d6a53047a #666 - Added enum to type 2024-02-13 10:10:50 +01:00
Elio Struyf
afb241ad6a #666 - Update schema 2024-02-13 09:51:20 +01:00
Elio Struyf
4229d262ae #666 - localization added 2024-02-13 09:40:27 +01:00
Elio Struyf
6b92a6f8b4 Merge pull request #753 from estruyf/issue/666
Issue/666
2024-02-13 09:29:21 +01:00
Elio Struyf
183e77b77b #666 - Support config splitting 2024-02-13 09:28:19 +01:00
Elio Struyf
da7d5e6854 Merge branch 'dev' into issue/666 2024-02-13 09:13:50 +01:00
Elio Struyf
8a08f54340 Added slug default value 2024-02-12 17:55:36 +01:00
Elio Struyf
be54b6286f #752 - use new field processing 2024-02-12 17:53:47 +01:00
Elio Struyf
1315602bcc #752 - Support placeholders in list field 2024-02-12 14:59:25 +01:00
Elio Struyf
0ad0179a4b Clean dashboard commands 2024-02-12 14:31:56 +01:00
Elio Struyf
9d68797c95 Update GH actions 2024-02-12 14:23:28 +01:00
Elio Struyf
ffaea3b55d #745 - Fix double date formatting 2024-02-12 14:02:13 +01:00
Elio Struyf
4565ea75ae Reverse spin 2024-02-12 13:51:24 +01:00
Elio Struyf
c4d3f76510 Fix menu positioning 2024-02-12 13:43:26 +01:00
Elio Struyf
ce2bd06f6d Merge pull request #740 from estruyf/poc/git-branching
New git actions
2024-02-12 13:05:38 +01:00
Elio Struyf
a29a6600ab Update welcome view 2024-02-12 13:05:26 +01:00
Elio Struyf
6cbf86f822 Add is repo 2024-02-12 13:02:39 +01:00
Elio Struyf
514272835a Merge branch 'dev' of github.com:estruyf/vscode-front-matter into poc/git-branching 2024-02-12 13:02:36 +01:00
Elio Struyf
3c29df54c1 Updated changelog 2024-02-12 12:36:08 +01:00
Elio Struyf
d06be0efa1 Merge branch 'dev' into poc/git-branching 2024-02-12 12:36:02 +01:00
Elio Struyf
2375be9211 Merge pull request #748 from estruyf/issue/747
Added placeholder support in slug field
2024-02-12 10:57:25 +01:00
Elio Struyf
b5b7dcf6b5 Merge branch 'dev' into issue/747 2024-02-12 10:57:19 +01:00
Elio Struyf
c81d5240f4 Merge pull request #750 from estruyf/issue/673
Git settings on welcome view and settings
2024-02-12 10:56:47 +01:00
Elio Struyf
06b8a579a8 #745 - Fix double date formatting 2024-02-08 11:45:15 +01:00
Elio Struyf
460c4964f6 #666 - First steps to implement media content types 2024-02-07 11:49:03 +01:00
Elio Struyf
d59d9a98d5 #746 - placeholder support for slug field 2024-01-30 21:45:01 +01:00
Elio Struyf
83cf0eb8f5 #746 - Placeholder support in slug 2024-01-29 16:58:25 +01:00
Elio Struyf
7c4aa1d63d Update changlog 2024-01-24 09:57:08 +01:00
Elio Struyf
6e84217458 #739 - localization keys + action button 2024-01-24 09:53:09 +01:00
Elio Struyf
b1380388b6 Issue: Open Preview button stops working #738 2024-01-23 20:10:48 +01:00
Elio Struyf
d70f983694 Optimizations 2024-01-23 15:21:04 +01:00
Elio Struyf
d22ebfa6ce Commit message input 2024-01-22 11:45:11 +01:00
Elio Struyf
cf96923d96 Faster loading for panel 2024-01-21 17:33:58 +01:00
Elio Struyf
6150a34547 Update git API 2024-01-21 17:27:56 +01:00
Elio Struyf
d45cd0d015 Git branching 2024-01-19 18:11:53 +01:00
Elio Struyf
36ae7081d1 Merge pull request #723 from estruyf/dev
Version 9.4.0
2023-12-12 16:29:45 +01:00
229 changed files with 7972 additions and 2603 deletions

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

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

View File

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

View File

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

View File

@@ -1,21 +1,61 @@
# Change Log
## [9.5.0] - 2024-xx-xx
## [10.1.0] - 2024-04-11 - [Release notes](https://beta.frontmatter.codes/updates/v10.1.0)
### ✨ New features
- [#671](https://github.com/estruyf/vscode-front-matter/issues/671): Command bar for contents and media dashboard
### 🎨 Enhancements
- [#773](https://github.com/estruyf/vscode-front-matter/issues/773): Added the ability to rename content files
- [#777](https://github.com/estruyf/vscode-front-matter/issues/777): Show an error in the metadata panel if something went wrong while parsing the front matter
- [#778](https://github.com/estruyf/vscode-front-matter/issues/778): Added the ability to open a file or webpage when custom scripts is completed
- [#783](https://github.com/estruyf/vscode-front-matter/issues/783): Always show the custom panel view
- [#785](https://github.com/estruyf/vscode-front-matter/issues/785): Adding common actions at the bottom of the content and media cards
- [#787](https://github.com/estruyf/vscode-front-matter/issues/787): Support for glob patterns in the page folder paths
- [#790](https://github.com/estruyf/vscode-front-matter/pull/790): Updated Japanese translations thanks to [mayumihara](https://github.com/mayumih387)
### 🐞 Fixes
- [#716](https://github.com/estruyf/vscode-front-matter/issues/716): Fix `dataFile` dropdown class
- [#768](https://github.com/estruyf/vscode-front-matter/issues/768): Update broken link to the documentation
- [#771](https://github.com/estruyf/vscode-front-matter/issues/771): Fix lowercase `data` tab label
- [#782](https://github.com/estruyf/vscode-front-matter/issues/782): Fix for setting the correct view when inserting media or snippets
- [#786](https://github.com/estruyf/vscode-front-matter/issues/786): Remove on startup as VSCode now triggers on known commands
## [10.0.2] - 2024-03-01
### 🐞 Fixes
- [#769](https://github.com/estruyf/vscode-front-matter/issues/769): Fix to remove internal properties for content folders
## [10.0.1] - 2024-02-28
### 🐞 Fixes
- [#766](https://github.com/estruyf/vscode-front-matter/issues/766): Fix for snippet placeholder retrieval
## [10.0.0] - 2024-02-28 - [Release notes](https://beta.frontmatter.codes/updates/v10.0.0)
### ✨ New features
- [#731](https://github.com/estruyf/vscode-front-matter/issues/731): Added the ability to map/unmap taxonomy to multiple pages at once
- [#746](https://github.com/estruyf/vscode-front-matter/issues/746): Placeholder support added to to the `slug` field
- [#749](https://github.com/estruyf/vscode-front-matter/issues/749): Ability to set your own filters on the content dashboard with the `frontMatter.content.filters` setting
- [#756](https://github.com/estruyf/vscode-front-matter/issues/756): i18n/multilingual content support
### 🎨 Enhancements
- [#673](https://github.com/estruyf/vscode-front-matter/pull/673): Added git settings to the welcome view and settings view
- [#727](https://github.com/estruyf/vscode-front-matter/pull/727): Updated Japanese translations thanks to [mayumihara](https://github.com/mayumih387)
- [#737](https://github.com/estruyf/vscode-front-matter/issues/737): Optimize the grid layout of the content and media dashboards
- [#739](https://github.com/estruyf/vscode-front-matter/pull/739): New Git settings to disable and require a commit message
- [#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
### ⚡️ Optimizations
- [#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

View File

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

View File

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

17
SUPPORT.md Normal file
View File

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

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5">
<path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="#424242">
<path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="#C5C5C5" width="16" height="16">
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" fill="#C5C5C5" width="16" height="16" class="w-6 h-6">
<path fill-rule="evenodd" d="M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z" clip-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 555 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="#424242" width="16" height="16">
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" fill="#424242" width="16" height="16" class="w-6 h-6">
<path fill-rule="evenodd" d="M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z" clip-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 555 B

View File

@@ -1,3 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="#C5C5C5" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.121 14.121L19 19m-7-7l7-7m-7 7l-2.879 2.879M12 12L9.121 9.121m0 5.758a3 3 0 10-4.243 4.243 3 3 0 004.243-4.243zm0-5.758a3 3 0 10-4.243-4.243 3 3 0 004.243 4.243z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" fill="#C5C5C5" width="16" height="16" class="w-6 h-6">
<path fill-rule="evenodd" d="M8.128 9.155a3.751 3.751 0 1 1 .713-1.321l1.136.656a.75.75 0 0 1 .222 1.104l-.006.007a.75.75 0 0 1-1.032.157 1.421 1.421 0 0 0-.113-.072l-.92-.531Zm-4.827-3.53a2.25 2.25 0 0 1 3.994 2.063.756.756 0 0 0-.122.23 2.25 2.25 0 0 1-3.872-2.293ZM13.348 8.272a5.073 5.073 0 0 0-3.428 3.57 5.08 5.08 0 0 0-.165 1.202 1.415 1.415 0 0 1-.707 1.201l-.96.554a3.751 3.751 0 1 0 .734 1.309l13.729-7.926a.75.75 0 0 0-.181-1.374l-.803-.215a5.25 5.25 0 0 0-2.894.05l-5.325 1.629Zm-9.223 7.03a2.25 2.25 0 1 0 2.25 3.897 2.25 2.25 0 0 0-2.25-3.897ZM12 12.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
<path d="M16.372 12.615a.75.75 0 0 1 .75 0l5.43 3.135a.75.75 0 0 1-.182 1.374l-.802.215a5.25 5.25 0 0 1-2.894-.051l-5.147-1.574a.75.75 0 0 1-.156-1.367l3-1.732Z" />
</svg>

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 939 B

View File

@@ -1,3 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="#424242" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.121 14.121L19 19m-7-7l7-7m-7 7l-2.879 2.879M12 12L9.121 9.121m0 5.758a3 3 0 10-4.243 4.243 3 3 0 004.243-4.243zm0-5.758a3 3 0 10-4.243-4.243 3 3 0 004.243 4.243z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" fill="#424242" width="16" height="16" class="w-6 h-6">
<path fill-rule="evenodd" d="M8.128 9.155a3.751 3.751 0 1 1 .713-1.321l1.136.656a.75.75 0 0 1 .222 1.104l-.006.007a.75.75 0 0 1-1.032.157 1.421 1.421 0 0 0-.113-.072l-.92-.531Zm-4.827-3.53a2.25 2.25 0 0 1 3.994 2.063.756.756 0 0 0-.122.23 2.25 2.25 0 0 1-3.872-2.293ZM13.348 8.272a5.073 5.073 0 0 0-3.428 3.57 5.08 5.08 0 0 0-.165 1.202 1.415 1.415 0 0 1-.707 1.201l-.96.554a3.751 3.751 0 1 0 .734 1.309l13.729-7.926a.75.75 0 0 0-.181-1.374l-.803-.215a5.25 5.25 0 0 0-2.894.05l-5.325 1.629Zm-9.223 7.03a2.25 2.25 0 1 0 2.25 3.897 2.25 2.25 0 0 0-2.25-3.897ZM12 12.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
<path d="M16.372 12.615a.75.75 0 0 1 .75 0l5.43 3.135a.75.75 0 0 1-.182 1.374l-.802.215a5.25 5.25 0 0 1-2.894-.051l-5.147-1.574a.75.75 0 0 1-.156-1.367l3-1.732Z" />
</svg>

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 939 B

View File

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

View File

@@ -105,7 +105,7 @@
"dashboard.header.tabs.contents": "Contenus",
"dashboard.header.tabs.media": "Médias",
"dashboard.header.tabs.snippets": "Snippets",
"dashboard.header.tabs.data": "données",
"dashboard.header.tabs.data": "Données",
"dashboard.header.tabs.taxonomies": "Taxonomies",
"dashboard.header.viewSwitch.toGrid": "Afficher en grille",
"dashboard.header.viewSwitch.toList": "Afficher en liste",

View File

@@ -105,7 +105,7 @@
"dashboard.header.tabs.contents": "Contenuto",
"dashboard.header.tabs.media": "Media",
"dashboard.header.tabs.snippets": "Snippets",
"dashboard.header.tabs.data": "dati",
"dashboard.header.tabs.data": "Dati",
"dashboard.header.tabs.taxonomies": "Tassonomie",
"dashboard.header.viewSwitch.toGrid": "Passa alla griglia",
"dashboard.header.viewSwitch.toList": "Passa all'elenco",

View File

@@ -3,8 +3,8 @@
"common.edit": "編集",
"common.delete": "削除",
"common.cancel": "キャンセル",
"common.clear": "クリア",
"common.apply": "適用",
"common.clear": "クリア",
"common.clear.value": "値をクリア",
"common.search": "検索",
"common.save": "保存",
@@ -35,6 +35,16 @@
"common.no": "いいえ",
"common.openSettings": "設定を開く",
"common.back": "戻る",
"common.open": "開く",
"common.openWithValue": "開く: {0}",
"common.openCustomActions": "カスタムコマンドを開く",
"common.view": "表示",
"common.translate": "翻訳する",
"common.languages": "言語",
"common.scripts": "スクリプト",
"common.rename": "ファイル名を変更する",
"loading.initPages": "記事を読み込んでいます",
"notifications.outputChannel.link": "出力ウィンドウ",
"notifications.outputChannel.description": "詳細は{0}を確認してください。",
@@ -42,18 +52,36 @@
"settings.view.common": "一般",
"settings.view.contentFolders": "記事フォルダー",
"settings.view.astro": "Astro",
"settings.view.integration": "統合機能",
"settings.openOnStartup": "起動時にダッシュボードを開く",
"settings.contentTypes": "記事タイプ",
"settings.contentFolders": "記事フォルダー",
"settings.diagnostic": "診断",
"settings.diagnostic.description": "診断プログラムを実行して、Front Matter CMS構成全体を確認できます。",
"settings.diagnostic.link": "完全診断を実行する",
"settings.git": "Git同期",
"settings.git.enabled": "Git同期を有効にして、変更内容をリポジトリと簡単に同期させます。",
"settings.git.commitMessage": "コミットメッセージ",
"settings.git.submoduleInfo": "Gitサブモジュールを使用している場合は、サブモジュールの設定についてドキュメントを参照してください。",
"settings.git.submoduleLink": "Gitサブモジュールについて確認する",
"settings.integration.title": "統合機能",
"settings.commonSettings.website.title": "ウェブサイトとSSGの設定",
"settings.commonSettings.previewUrl": "プレビュー用URL",
"settings.commonSettings.websiteUrl": "ウェブサイトのURL",
"settings.commonSettings.startCommand": "SSG/フレームワーク起動コマンド",
"settings.integrationsView.deepl.title": "DeepL",
"settings.integrationsView.deepl.intput.label": "API key",
"settings.integrationsView.deepl.intput.placeholder": "DeepL API keyを入力",
"settings.integrationsView.azure.title": "Azure AI Translator",
"settings.integrationsView.azure.intput.label": "サブスクリプションキー",
"settings.integrationsView.azure.intput.placeholder": "Azure AI Translatorのサブスクリプションキーを入力",
"settings.integrationsView.azure.region.label": "リージョン",
"settings.integrationsView.azure.region.placeholder": "Azure AI Translatorのリージョンを入力 例: westeurope",
"developer.title": "開発モード",
"developer.reload.title": "ダッシュボードを再読み込み",
"developer.reload.label": "再読み込み",
@@ -81,6 +109,8 @@
"dashboard.contents.contentActions.menuItem.view": "開く",
"dashboard.contents.contentActions.alert.title": "削除: {0}",
"dashboard.contents.contentActions.alert.description": "本当に\"{0}\"を削除しますか?",
"dashboard.contents.contentActions.translations.create": "翻訳する",
"dashboard.contents.contentActions.translations.menu": "翻訳版",
"dashboard.contents.item.invalidTitle": "<無効なタイトル>",
"dashboard.contents.item.invalidDescription": "<無効なディスクリプション>",
@@ -108,6 +138,7 @@
"dashboard.dataView.dataView.getStarted": "データタイプを選択して開始する",
"dashboard.dataView.dataView.noDataFiles": "データファイルが見つかりませんでした",
"dashboard.dataView.dataView.getStarted.link": "データファイルの利用方法について確認する",
"dashboard.dataView.dataView.update.message": "データエントリーを更新しました。",
"dashboard.dataView.emptyView.heading": "最初にデータタイプを選んでください",
@@ -118,6 +149,13 @@
"dashboard.errorView.description": "ダッシュボードを一旦閉じてからやり直してください。",
"dashboard.filters.languageFilter.label": "ロケール",
"dashboard.filters.languageFilter.all": "全て",
"dashboard.header.actionsBar.itemsSelected": "{0}件を選択中",
"dashboard.header.actionsBar.alertDelete.title": "選択ファイルを削除",
"dashboard.header.actionsBar.alertDelete.description": "選択したファイルを本当に削除しますか?",
"dashboard.header.breadcrumb.home": "ホーム",
"dashboard.header.clearFilters.title": "絞り込み・グループ・並べ替えを解除",
@@ -201,10 +239,17 @@
"dashboard.media.folderCreation.hexo.create": "Assetフォルダーを作成",
"dashboard.media.folderCreation.folder.create": "新規フォルダーを作成",
"dashboard.media.folderItem.contentDirectory": "コンテンツディレクトリー",
"dashboard.media.folderItem.publicDirectory": "Publicディレクトリー",
"dashboard.media.item.buttom.insert.image": "画像を挿入",
"dashboard.media.item.buttom.insert.snippet": "スニペットを挿入",
"dashboard.media.item.quickAction.insert.field": "この画像を\"{0}\"フィールドに追加",
"dashboard.media.item.quickAction.insert.markdown": "画像をMarkdown記法で挿入",
"dashboard.media.item.quickAction.copy.path": "ファイルパスをコピー",
"dashboard.media.item.quickAction.delete": "ファイルを削除",
"dashboard.media.item.menuItem.view": "メタデータの詳細を表示",
"dashboard.media.item.menuItem.edit.metadata": "メタデータを編集",
"dashboard.media.item.menuItem.insert.image": "画像を挿入",
"dashboard.media.item.menuItem.reveal.media": "メディアの場所を表示",
@@ -275,6 +320,8 @@
"dashboard.steps.stepsToGetStarted.contentFolders.information.description": "エクスプローラーでフォルダー名を右クリックして「フォルダーを登録」を選択する方法でも、フォルダーの登録が可能です。",
"dashboard.steps.stepsToGetStarted.tags.name": "全てのタグとカテゴリーをインポート(オプション)",
"dashboard.steps.stepsToGetStarted.tags.description": "Front Matterに記事用フォルダーが登録されました。記事から全てのタグとカテゴリーをインポートしますか",
"dashboard.steps.stepsToGetStarted.git.name": "Git同期を有効化しますか",
"dashboard.steps.stepsToGetStarted.git.description": "Git同期を有効にして、変更内容をリポジトリと簡単に同期させます。",
"dashboard.steps.stepsToGetStarted.showDashboard.name": "ダッシュボードを開く",
"dashboard.steps.stepsToGetStarted.showDashboard.description": "全ての設定が終わると、ダッシュボードが表示できるようになります。",
"dashboard.steps.stepsToGetStarted.template.name": "設定用のテンプレートを使用する",
@@ -283,6 +330,7 @@
"dashboard.steps.stepsToGetStarted.astroContentTypes.name": "Astroコンテンツコレクションのコンテンツタイプを作成する",
"dashboard.taxonomyView.button.add.title": "\"{0}\"をタクソノミーに追加",
"dashboard.taxonomyView.button.tag.title": "タグを追加",
"dashboard.taxonomyView.button.edit.title": "\"{0}\"を編集",
"dashboard.taxonomyView.button.merge.title": "\"{0}\"をマージ",
"dashboard.taxonomyView.button.move.title": "他のタクソノミーへ移行",
@@ -329,6 +377,11 @@
"dashboard.configuration.astro.astroContentTypes.empty": "Astroコンテンツコレクションが見つかりません。",
"dashboard.configuration.astro.astroContentTypes.description": "以下のAstroコンテンツコレクションは、コンテンツタイプを生成するために使用できます。",
"panel.git.gitAction.title": "変更の反映",
"panel.git.gitAction.branch.select": "ブランチを選択",
"panel.git.gitAction.input.placeholder": "コミットメッセージ",
"panel.git.gitAction.button.fetch": "フェッチ",
"panel.contentType.contentTypeValidator.title": "記事タイプ",
"panel.contentType.contentTypeValidator.hint": "記事タイプのフィールドは設定と異なります。この記事の記事タイプを、作成・更新または設定しますか?",
"panel.contentType.contentTypeValidator.button.create": "新しい記事タイプを作成",
@@ -414,6 +467,7 @@
"panel.globalSettings.action.server.placeholder": "例: {0}",
"panel.metadata.title": "メタデータ",
"panel.metadata.focusProblems": "詳細を「問題」表示で確認してください。",
"panel.otherActions.title": "他のコマンド",
"panel.otherActions.writingSettings.enabled": "ライティング設定が有効",
@@ -474,6 +528,10 @@
"commands.article.setDate.error": "日付の表示形式の解析中に何らかの問題が発生しました。\"{0}\"の設定を確認してください。",
"commands.article.updateSlug.error": "ファイル名を変更できませんでした。: {0}",
"commands.article.rename.fileNotExists.error": "ファイルが存在しません。",
"commands.article.rename.fileExists.error": "\"{0}\" というファイル名は既に存在しています。",
"commands.article.rename.fileName.title": "ファイル名を変更: {0}",
"commands.article.rename.fileName.prompt": "ファイル名",
"commands.cache.cleared": "キャッシュがクリアされました。",
@@ -501,6 +559,19 @@
"commands.folders.get.notificationError.remove.action": "フォルダー設定を削除",
"commands.folders.get.notificationError.create.action": "フォルダーを作成",
"commands.i18n.create.warning.noFileSelected": "ファイルが選択されていません。",
"commands.i18n.create.warning.noFile": "ファイルが取得できませんでした。",
"commands.i18n.create.warning.noContentType": "現在のファイルの記事タイプを取得できませんでした。",
"commands.i18n.create.warning.noConfig": "i18nの設定が見つかりません。",
"commands.i18n.create.error.noLocaleDefinition": "現在のファイルのロケールを取得できませんでした。",
"commands.i18n.create.error.noLocales": "現在のファイルは利用可能なすべての言語に翻訳されています。",
"commands.i18n.create.error.noContentFolder": "現在のファイルの記事フォルダーを指定できませんでした。",
"commands.i18n.create.error.fileExists": "そのi18n翻訳は既に存在しています。",
"commands.i18n.create.success.created": "\"{0}\" i18n記事ファイルを作成しました。",
"commands.i18n.create.quickPick.title": "言語別の記事を作成",
"commands.i18n.create.quickPick.placeHolder": "どの言語で記事を作成しますか?",
"commands.i18n.translate.progress.title": "記事を翻訳しています...",
"commands.preview.panel.title": "プレビュー: {0}",
"commands.preview.askUserToPickFolder.title": "プレビュー用の記事フォルダーを選択してください。",
@@ -611,9 +682,6 @@
"helpers.extension.getVersion.changelog": "変更履歴を確認する",
"helpers.extension.getVersion.starIt": "⭐️を付ける",
"helpers.extension.getVersion.update.notification": "{0} が v{1} に更新されました!新機能をチェックしてください!",
"helpers.extension.migrateSettings.deprecated.warning": "\"{0}\"及び\"{1}\"の設定は非推奨になりました。代わりに\"isPublishDate\"と\"isModifiedDate\"の日付フィールドを使用してください。",
"helpers.extension.migrateSettings.deprecated.warning.hide": "非表示にする",
"helpers.extension.migrateSettings.deprecated.warning.seeGuide": "移行ガイドを読む",
"helpers.extension.migrateSettings.templates.quickPick.title": "{0} - テンプレート",
"helpers.extension.migrateSettings.templates.quickPick.placeholder": "テンプレート機能の使用を継続しますか?",
"helpers.extension.checkIfExtensionCanRun.warning": "Front MatterのBETA版は安定版がインストールされている場合は利用できません。BETA版のみがインストールされていることを確認してください。",
@@ -648,6 +716,7 @@
"helpers.questions.selectContentType.quickPick.title": "記事タイプ",
"helpers.questions.selectContentType.quickPick.placeholder": "新規作成する記事の記事タイプを選択してください。",
"helpers.questions.selectContentType.noSelection.warning": "記事タイプが選択されていません。",
"helpers.questions.selectContentType.quickPick.error.noContentTypes": "このフォルダーには、一致する記事タイプが設定されていません。",
"helpers.seoHelper.checkLength.diagnostic.message": "記事{0}の文字数が{1}文字を超えています(現在の文字数: {2}。SEOの観点上、{1}文字以内に収めることが推奨されます。",
@@ -690,6 +759,7 @@
"listeners.dashboard.settingsListener.triggerTemplate.progress.title": "テンプレートをダウンロードして初期化しています...",
"listeners.dashboard.settingsListener.triggerTemplate.download.error": "テンプレートのダウンロードに失敗しました。",
"listeners.dashboard.settingsListener.triggerTemplate.init.error": "テンプレートの初期化に失敗しました。",
"listeners.dashboard.settingsListener.setSecretValue.message": "設定が更新されました。",
"listeners.dashboard.snippetListener.addSnippet.missingFields.warning": "スニペットのタイトルまたはbodyが空です。",
"listeners.dashboard.snippetListener.addSnippet.exists.warning": "同じタイトルのスニペットが既に存在しています。",
@@ -700,6 +770,7 @@
"listeners.panel.dataListener.aiSuggestTaxonomy.noEditor.error": "アクティブなエディターがありません。",
"listeners.panel.dataListener.aiSuggestTaxonomy.noData.error": "記事データがありません。",
"listeners.panel.dataListener.getDataFileEntries.noDataFiles.error": "データファイルのエントリーが見つかりませんでした。",
"listeners.panel.dataListener.pushMetadata.frontMatter.error": "front matterの解析中にエラーが発生しまいた。ファイルの内容を確認してください。",
"listeners.panel.taxonomyListener.aiSuggestTaxonomy.noEditor.error": "アクティブなエディターがありません。",

View File

@@ -35,6 +35,14 @@
"common.no": "no",
"common.openSettings": "Open settings",
"common.back": "Back",
"common.open": "Open",
"common.openWithValue": "Open: {0}",
"common.openCustomActions": "Open custom actions",
"common.view": "View",
"common.translate": "Translate",
"common.languages": "Languages",
"common.scripts": "Scripts",
"common.rename": "Rename",
"loading.initPages": "Loading content",
@@ -44,6 +52,8 @@
"settings.view.common": "Common",
"settings.view.contentFolders": "Content folders",
"settings.view.astro": "Astro",
"settings.view.integration": "Integration",
"settings.openOnStartup": "Open dashboard on startup",
"settings.contentTypes": "Content types",
"settings.contentFolders": "Content folders",
@@ -55,12 +65,23 @@
"settings.git.commitMessage": "Commit message",
"settings.git.submoduleInfo": "When working with Git submodules, you can refer to the submodule settings in the documentation.",
"settings.git.submoduleLink": "Read more about Git submodules",
"settings.integration.title": "Integration",
"settings.commonSettings.website.title": "Website and SSG settings",
"settings.commonSettings.previewUrl": "Preview URL",
"settings.commonSettings.websiteUrl": "Website URL",
"settings.commonSettings.startCommand": "SSG/Framework start command",
"settings.integrationsView.deepl.title": "DeepL",
"settings.integrationsView.deepl.intput.label": "API key",
"settings.integrationsView.deepl.intput.placeholder": "Enter your Deepl API key",
"settings.integrationsView.azure.title": "Azure AI Translator Service",
"settings.integrationsView.azure.intput.label": "Subscription key",
"settings.integrationsView.azure.intput.placeholder": "Enter your Azure AI Translator - Subscription key",
"settings.integrationsView.azure.region.label": "Region",
"settings.integrationsView.azure.region.placeholder": "Enter your Azure AI Translator - Region. Example: westeurope",
"developer.title": "Developer mode",
"developer.reload.title": "Reload the dashboard",
"developer.reload.label": "Reload",
@@ -88,6 +109,8 @@
"dashboard.contents.contentActions.menuItem.view": "View",
"dashboard.contents.contentActions.alert.title": "Delete: {0}",
"dashboard.contents.contentActions.alert.description": "Are you sure you want to delete the \"{0}\" content?",
"dashboard.contents.contentActions.translations.create": "Create translation",
"dashboard.contents.contentActions.translations.menu": "Translations",
"dashboard.contents.item.invalidTitle": "<invalid title>",
"dashboard.contents.item.invalidDescription": "<invalid description>",
@@ -126,6 +149,13 @@
"dashboard.errorView.description": "Please close the dashboard and try again.",
"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",
@@ -179,7 +209,7 @@
"dashboard.header.tabs.contents": "Contents",
"dashboard.header.tabs.media": "Media",
"dashboard.header.tabs.snippets": "Snippets",
"dashboard.header.tabs.data": "data",
"dashboard.header.tabs.data": "Data",
"dashboard.header.tabs.taxonomies": "Taxonomies",
"dashboard.header.viewSwitch.toGrid": "Change to grid",
@@ -209,10 +239,17 @@
"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",
"dashboard.media.item.quickAction.insert.field": "Insert image for your \"{0}\" field",
"dashboard.media.item.quickAction.insert.markdown": "Insert image with markdown markup",
"dashboard.media.item.quickAction.copy.path": "Copy media path",
"dashboard.media.item.quickAction.delete": "Delete media file",
"dashboard.media.item.menuItem.view": "View media details",
"dashboard.media.item.menuItem.edit.metadata": "Edit metadata",
"dashboard.media.item.menuItem.insert.image": "Insert image",
"dashboard.media.item.menuItem.reveal.media": "Reveal media",
@@ -293,6 +330,7 @@
"dashboard.steps.stepsToGetStarted.astroContentTypes.name": "Create Content-Types for your Astro Content Collections",
"dashboard.taxonomyView.button.add.title": "Add {0} to taxonomy settings",
"dashboard.taxonomyView.button.tag.title": "Tag content",
"dashboard.taxonomyView.button.edit.title": "Edit {0}",
"dashboard.taxonomyView.button.merge.title": "Merge {0}",
"dashboard.taxonomyView.button.move.title": "Move to another taxonomy type",
@@ -339,6 +377,11 @@
"dashboard.configuration.astro.astroContentTypes.empty": "No Astro Content Collections found.",
"dashboard.configuration.astro.astroContentTypes.description": "The following Astro Content Collections can be used to generate a content-type.",
"panel.git.gitAction.title": "Publish changes",
"panel.git.gitAction.branch.select": "Select branch",
"panel.git.gitAction.input.placeholder": "Commit message",
"panel.git.gitAction.button.fetch": "Fetch",
"panel.contentType.contentTypeValidator.title": "Content-type",
"panel.contentType.contentTypeValidator.hint": "We noticed field differences between the content-type and the front matter data. \n Would you like to create, update, or set the content-type for this content?",
"panel.contentType.contentTypeValidator.button.create": "Create content-type",
@@ -424,6 +467,7 @@
"panel.globalSettings.action.server.placeholder": "Example: {0}",
"panel.metadata.title": "Metadata",
"panel.metadata.focusProblems": "Check the problems view for more information",
"panel.otherActions.title": "Other actions",
"panel.otherActions.writingSettings.enabled": "Writing settings enabled",
@@ -484,6 +528,10 @@
"commands.article.setDate.error": "Something failed while parsing the date format. Check your \"{0}\" setting.",
"commands.article.updateSlug.error": "Failed to rename file: {0}",
"commands.article.rename.fileNotExists.error": "The file did not exist",
"commands.article.rename.fileExists.error": "A file with the name \"{0}\" already exists",
"commands.article.rename.fileName.title": "Rename: {0}",
"commands.article.rename.fileName.prompt": "File name",
"commands.cache.cleared": "Cache cleared",
@@ -511,6 +559,19 @@
"commands.folders.get.notificationError.remove.action": "Remove folder",
"commands.folders.get.notificationError.create.action": "Create folder",
"commands.i18n.create.warning.noFileSelected": "No file selected.",
"commands.i18n.create.warning.noFile": "The file could not be retrieved.",
"commands.i18n.create.warning.noContentType": "Content type could not be retrieved for the current file.",
"commands.i18n.create.warning.noConfig": "No i18n configuration found.",
"commands.i18n.create.error.noLocaleDefinition": "Could not retrieve the locale for the current file.",
"commands.i18n.create.error.noLocales": "Current file has been translated to all available languages.",
"commands.i18n.create.error.noContentFolder": "Could not define a content folder for the current file.",
"commands.i18n.create.error.fileExists": "The i18n translation already exists.",
"commands.i18n.create.success.created": "Created \"{0}\" i18n content file.",
"commands.i18n.create.quickPick.title": "Create content for locale",
"commands.i18n.create.quickPick.placeHolder": "To which locale do you want to create a new content?",
"commands.i18n.translate.progress.title": "Translating content...",
"commands.preview.panel.title": "Preview: {0}",
"commands.preview.askUserToPickFolder.title": "Select the folder of the article to preview",
@@ -621,9 +682,6 @@
"helpers.extension.getVersion.changelog": "Check the changelog",
"helpers.extension.getVersion.starIt": "Give it a ⭐️",
"helpers.extension.getVersion.update.notification": "{0} has been updated to v{1} — check out what's new!",
"helpers.extension.migrateSettings.deprecated.warning": "The \"{0}\" and \"{1}\" settings have been deprecated. Please use the \"isPublishDate\" and \"isModifiedDate\" datetime field properties instead.",
"helpers.extension.migrateSettings.deprecated.warning.hide": "Hide",
"helpers.extension.migrateSettings.deprecated.warning.seeGuide": "See migration guide",
"helpers.extension.migrateSettings.templates.quickPick.title": "{0} - Templates",
"helpers.extension.migrateSettings.templates.quickPick.placeholder": "Do you want to keep on using the template functionality?",
"helpers.extension.checkIfExtensionCanRun.warning": "Front Matter BETA cannot be used while the stable version is installed. Please ensure that you have only over version installed.",
@@ -658,6 +716,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.",
@@ -700,6 +759,7 @@
"listeners.dashboard.settingsListener.triggerTemplate.progress.title": "Downloading and initializing the template...",
"listeners.dashboard.settingsListener.triggerTemplate.download.error": "Failed to download the template.",
"listeners.dashboard.settingsListener.triggerTemplate.init.error": "Failed to initialize the template.",
"listeners.dashboard.settingsListener.setSecretValue.message": "Setting has been updated.",
"listeners.dashboard.snippetListener.addSnippet.missingFields.warning": "Snippet missing title or body",
"listeners.dashboard.snippetListener.addSnippet.exists.warning": "Snippet with the same title already exists",
@@ -710,6 +770,7 @@
"listeners.panel.dataListener.aiSuggestTaxonomy.noEditor.error": "No active editor",
"listeners.panel.dataListener.aiSuggestTaxonomy.noData.error": "No article data",
"listeners.panel.dataListener.getDataFileEntries.noDataFiles.error": "Couldn't find data file entries",
"listeners.panel.dataListener.pushMetadata.frontMatter.error": "Something went wrong while parsing your front matter. Please check the contents of your file.",
"listeners.panel.taxonomyListener.aiSuggestTaxonomy.noEditor.error": "No active editor",

874
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,15 +3,14 @@
"displayName": "Front Matter CMS",
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Docusaurus, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "9.5.0",
"version": "10.1.0",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
"color": "#0e131f",
"theme": "dark"
},
"badges": [
{
"badges": [{
"description": "version",
"url": "https://img.shields.io/github/package-json/v/estruyf/vscode-front-matter?color=green&label=vscode-front-matter&style=flat-square",
"href": "https://github.com/estruyf/vscode-front-matter"
@@ -51,8 +50,7 @@
},
"activationEvents": [
"workspaceContains:**/.frontmatter",
"workspaceContains:**/frontmatter.json",
"onStartupFinished"
"workspaceContains:**/frontmatter.json"
],
"main": "./dist/extension.js",
"contributes": {
@@ -71,12 +69,16 @@
"**/.frontmatter/config/*.json": "jsonc"
}
},
"keybindings": [
{
"keybindings": [{
"command": "frontMatter.dashboard",
"key": "alt+d"
},
{
"command": "workbench.action.webview.reloadWebviewAction",
"key": "ctrl+r",
"mac": "cmd+r",
"when": "activeWebviewPanelId == frontMatterDashboard"
}, {
"command": "frontMatter.insertMedia",
"key": "ctrl+shift+i",
"mac": "cmd+shift+i",
@@ -90,23 +92,19 @@
}
],
"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%",
@@ -174,8 +172,7 @@
"frontMatter.content.defaultFileType": {
"type": "string",
"default": "md",
"oneOf": [
{
"oneOf": [{
"enum": [
"md",
"mdx"
@@ -191,8 +188,7 @@
"frontMatter.content.defaultSorting": {
"type": "string",
"default": "",
"oneOf": [
{
"oneOf": [{
"enum": [
"LastModifiedAsc",
"LastModifiedDesc",
@@ -311,6 +307,17 @@
"type": "boolean",
"default": false,
"description": "%setting.frontMatter.content.pageFolders.items.properties.disableCreation.description%"
},
"defaultLocale": {
"type": "string",
"description": "%setting.frontMatter.content.pageFolders.items.properties.defaultLocale.description%"
},
"locales": {
"type": "array",
"description": "%setting.frontMatter.content.pageFolders.items.properties.locales.description%",
"items": {
"$ref": "#i18n"
}
}
},
"additionalProperties": false,
@@ -321,6 +328,35 @@
},
"scope": "Content"
},
"frontMatter.content.i18n": {
"type": "array",
"default": [],
"markdownDescription": "%setting.frontMatter.content.i18n.markdownDescription%",
"items": {
"$id": "#i18n",
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "%setting.frontMatter.content.i18n.items.properties.title.description%"
},
"locale": {
"type": "string",
"description": "%setting.frontMatter.content.i18n.items.properties.locale.description%"
},
"path": {
"type": "string",
"description": "%setting.frontMatter.content.i18n.items.properties.path.description%"
}
},
"additionalProperties": false,
"required": [
"locale",
"path"
]
},
"scope": "Content"
},
"frontMatter.content.placeholders": {
"type": "array",
"default": [],
@@ -495,14 +531,18 @@
"frontMatter.content.filters": {
"type": "array",
"default": [
"pageFolders",
"contentFolders",
"tags",
"categories"
],
"markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%",
"items": [
{
"type": "string"
"items": [{
"type": "string",
"enum": [
"contentFolders",
"tags",
"categories"
]
},
{
"type": "object",
@@ -569,8 +609,7 @@
"command": {
"$id": "#scriptCommand",
"type": "string",
"anyOf": [
{
"anyOf": [{
"enum": [
"node",
"bash",
@@ -672,17 +711,6 @@
"default": "",
"markdownDescription": "%setting.frontMatter.dashboard.content.card.fields.title.markdownDescription%"
},
"frontMatter.dashboard.mediaSnippet": {
"type": "array",
"default": [],
"markdownDescription": "%setting.frontMatter.dashboard.mediaSnippet.markdownDescription%",
"deprecationMessage": "%setting.frontMatter.dashboard.mediaSnippet.deprecationMessage%",
"items": {
"type": "string",
"description": "%setting.frontMatter.dashboard.mediaSnippet.items.description%"
},
"scope": "dashboard"
},
"frontMatter.dashboard.openOnStart": {
"type": [
"boolean",
@@ -777,8 +805,7 @@
"title",
"file"
],
"anyOf": [
{
"anyOf": [{
"required": [
"schema"
]
@@ -832,8 +859,7 @@
"id",
"path"
],
"anyOf": [
{
"anyOf": [{
"required": [
"schema"
]
@@ -899,6 +925,22 @@
"markdownDescription": "%setting.frontMatter.git.commitMessage.markdownDescription%",
"default": "Synced by Front Matter"
},
"frontMatter.git.disableOnBranches": {
"type": "array",
"markdownDescription": "%setting.frontMatter.git.disableOnBranches.markdownDescription%",
"default": [],
"items": {
"type": "string"
}
},
"frontMatter.git.requiresCommitMessage": {
"type": "array",
"markdownDescription": "%setting.frontMatter.git.requiresCommitMessage.markdownDescription%",
"default": [],
"items": {
"type": "string"
}
},
"frontMatter.git.submodule.pull": {
"type": "boolean",
"markdownDescription": "%setting.frontMatter.git.submodule.pull.markdownDescription%",
@@ -1007,6 +1049,79 @@
"markdownDescription": "%setting.frontMatter.media.defaultSorting.markdownDescription%",
"scope": "Content"
},
"frontMatter.media.contentTypes": {
"type": [
"array",
"null"
],
"markdownDescription": "%setting.frontMatter.media.contentTypes.markdownDescription%",
"items": {
"type": "object",
"description": "%setting.frontMatter.media.contentTypes.items.description%",
"properties": {
"name": {
"type": "string",
"description": "%setting.frontMatter.media.contentTypes.items.properties.name.description%"
},
"fileTypes": {
"type": "array",
"description": "%setting.frontMatter.media.contentTypes.items.properties.fileTypes.description%",
"items": {
"type": "string"
}
},
"fields": {
"type": "array",
"description": "%setting.frontMatter.media.contentTypes.items.properties.fields.description%",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "%setting.frontMatter.media.contentTypes.items.properties.fields.properties.title.description%"
},
"name": {
"type": "string",
"description": "%setting.frontMatter.media.contentTypes.items.properties.fields.properties.name.description%"
},
"type": {
"type": "string",
"enum": [
"string"
],
"description": "%setting.frontMatter.media.contentTypes.items.properties.fields.properties.type.description%"
},
"single": {
"type": "boolean",
"description": "%setting.frontMatter.media.contentTypes.items.properties.fields.properties.single.description%"
}
}
}
}
}
},
"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": {
"type": "array",
"default": [
@@ -1234,8 +1349,7 @@
"default": "",
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%",
"not": {
"anyOf": [
{
"anyOf": [{
"const": ""
},
{
@@ -1429,8 +1543,7 @@
"type",
"name"
],
"allOf": [
{
"allOf": [{
"if": {
"properties": {
"type": {
@@ -1601,6 +1714,14 @@
"default": null,
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.previewPath.description%"
},
"slugTemplate": {
"type": [
"null",
"string"
],
"default": null,
"description": "%setting.frontMatter.content.pageFolders.items.properties.slugTemplate.description%"
},
"template": {
"type": "string",
"default": "",
@@ -1630,51 +1751,48 @@
"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": {
@@ -1687,8 +1805,7 @@
"type": "string",
"description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%",
"not": {
"anyOf": [
{
"anyOf": [{
"const": ""
},
{
@@ -1716,12 +1833,6 @@
},
"scope": "Taxonomy"
},
"frontMatter.taxonomy.dateField": {
"type": "string",
"default": "date",
"markdownDescription": "%setting.frontMatter.taxonomy.dateField.markdownDescription%",
"deprecationMessage": "%setting.frontMatter.taxonomy.dateField.deprecationMessage%"
},
"frontMatter.taxonomy.dateFormat": {
"type": "string",
"markdownDescription": "%setting.frontMatter.taxonomy.dateFormat.markdownDescription%",
@@ -1775,12 +1886,6 @@
"markdownDescription": "%setting.frontMatter.taxonomy.indentArrays.markdownDescription%",
"scope": "Taxonomy"
},
"frontMatter.taxonomy.modifiedField": {
"type": "string",
"default": "lastmod",
"markdownDescription": "%setting.frontMatter.taxonomy.modifiedField.markdownDescription%",
"deprecationMessage": "%setting.frontMatter.taxonomy.modifiedField.deprecationMessage%"
},
"frontMatter.taxonomy.quoteStringValues": {
"type": "boolean",
"default": false,
@@ -1843,6 +1948,11 @@
"markdownDescription": "%setting.frontMatter.taxonomy.slugSuffix.markdownDescription%",
"scope": "Taxonomy"
},
"frontMatter.taxonomy.slugTemplate": {
"type": "string",
"markdownDescription": "%setting.frontMatter.taxonomy.slugTemplate.markdownDescription%",
"scope": "Taxonomy"
},
"frontMatter.taxonomy.tags": {
"type": "array",
"markdownDescription": "%setting.frontMatter.taxonomy.tags.markdownDescription%",
@@ -1880,8 +1990,7 @@
}
}
},
"commands": [
{
"commands": [{
"command": "frontMatter.project.switch",
"title": "%command.frontMatter.project.switch%",
"category": "Front Matter",
@@ -2196,23 +2305,27 @@
"command": "frontMatter.cache.clear",
"title": "%command.frontMatter.cache.clear%",
"category": "Front Matter"
}
],
"submenus": [
},
{
"id": "frontmatter.submenu",
"label": "Front Matter"
"command": "frontMatter.i18n.create",
"title": "%command.frontMatter.i18n.create%",
"category": "Front Matter",
"icon": {
"light": "assets/icons/i18n-light.svg",
"dark": "assets/icons/i18n-dark.svg"
}
}
],
"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"
@@ -2242,6 +2355,11 @@
"group": "navigation@-128",
"when": "frontMatter:file:isValid == true"
},
{
"command": "frontMatter.i18n.create",
"group": "navigation@-127",
"when": "frontMatter:file:isValid && frontMatter:i18n:enabled"
},
{
"command": "frontMatter.markup.options",
"group": "navigation@-126",
@@ -2293,14 +2411,11 @@
"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"
@@ -2316,8 +2431,7 @@
"group": "frontmatter@3"
}
],
"commandPalette": [
{
"commandPalette": [{
"command": "frontMatter.init",
"when": "frontMatterCanInit"
},
@@ -2325,14 +2439,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"
@@ -2345,10 +2451,30 @@
"command": "frontMatter.git.sync",
"when": "frontMatter:git:enabled"
},
{
"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"
@@ -2409,10 +2535,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"
@@ -2433,14 +2587,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"
@@ -2457,17 +2603,12 @@
"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"
@@ -2499,57 +2640,52 @@
}
]
},
"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"
]
"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"
},
{
"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": [
"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.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"
]
}
]
}
]
"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"
]
}
]
}]
},
"scripts": {
"dev:ext": "npm run clean && npm run localization:generate && npm-run-all --parallel watch:*",
@@ -2575,13 +2711,11 @@
},
"devDependencies": {
"@actions/core": "^1.10.0",
"@bendera/vscode-webview-elements": "0.6.2",
"@estruyf/vscode": "^1.1.0",
"@headlessui/react": "^1.7.17",
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1",
"@iarna/toml": "2.2.3",
"@octokit/rest": "^18.12.0",
"@popperjs/core": "^2.11.6",
"@sentry/react": "^6.19.7",
"@sentry/tracing": "^6.19.7",
"@tailwindcss/forms": "^0.5.3",
@@ -2601,6 +2735,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",
@@ -2609,6 +2744,7 @@
"assert": "^2.0.0",
"autoprefixer": "^10.4.13",
"cheerio": "1.0.0-rc.12",
"clsx": "^2.1.0",
"css-loader": "5.2.7",
"date-fns": "2.23.0",
"dotenv": "^16.3.1",
@@ -2644,17 +2780,18 @@
"react-dom": "17.0.1",
"react-dropzone": "^11.7.1",
"react-markdown": "^8.0.7",
"react-popper": "^2.3.0",
"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",
"simple-git": "^3.16.0",
"style-loader": "2.0.0",
"tailwind-merge": "^2.2.1",
"tailwindcss": "^3.2.4",
"tailwindcss-animate": "^1.0.7",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
"uniforms": "^3.10.2",
@@ -2673,5 +2810,8 @@
},
"vsce": {
"dependencies": false
},
"dependencies": {
"@radix-ui/react-dropdown-menu": "^2.0.6"
}
}
}

View File

@@ -48,6 +48,7 @@
"command.frontMatter.markup.unorderedlist": "順序なしリスト",
"command.frontMatter.git.sync": "同期",
"command.frontMatter.cache.clear": "キャッシュをクリア",
"command.frontMatter.i18n.create": "新しい翻訳を作成",
"settings.configuration.title": "Front Matter: チームで作業する場合はfrontmatter.jsonで設定してください。",
"setting.frontMatter.projects.markdownDescription": "Front Matter CMSを利用するプロジェクトを設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.projects)",
"setting.frontMatter.projects.items.properties.name.markdownDescription": "プロジェクトの名前を指定します。",
@@ -60,7 +61,7 @@
"setting.frontMatter.content.defaultFileType.markdownDescription": "新しい記事を作成する際の既定のファイル形式を設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.content.defaultfiletype)",
"setting.frontMatter.content.defaultSorting.markdownDescription": "ダッシュボード上に表示される記事一覧の既定の並び順を設定します。Enum列挙型や任意のIDを指定してください。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.content.defaultsorting)",
"setting.frontMatter.content.draftField.markdownDescription": "記事の下書きステータスを管理するフィールドを設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.content.draftfield)",
"setting.frontMatter.content.draftField.properties.type.description": "使用する下書きフィールドの種類",
"setting.frontMatter.content.draftField.properties.type.description": "使用する下書きフィールドの",
"setting.frontMatter.content.draftField.properties.name.description": "使用するフィールドの名前",
"setting.frontMatter.content.draftField.properties.invert.description": "既定では、記事が下書きの場合、下書きフィールドは true に設定されます。これを true に設定すると、false に設定されます。",
"setting.frontMatter.content.draftField.properties.choices.description": "フィールドの選択肢のリスト",
@@ -75,6 +76,12 @@
"setting.frontMatter.content.pageFolders.items.properties.filePrefix.description": "ファイル名の接頭辞を定義します。",
"setting.frontMatter.content.pageFolders.items.properties.contentTypes.description": "現在の場所に使用できる記事タイプを定義します。定義しない場合は、全ての記事タイプを使用できます。",
"setting.frontMatter.content.pageFolders.items.properties.disableCreation.description": "フォルダー内の新しい記事の作成を無効にします。",
"setting.frontMatter.content.pageFolders.items.properties.defaultLocale.description": "ページフォルダー用デフォルトのロケールIDを設定します。このフォルダー内の全ての記事が`frontMatter.content.i18n`で設定された言語へ翻訳可能になります。",
"setting.frontMatter.content.pageFolders.items.properties.locales.description": "ページフォルダーで利用するロケールを設定します。この設定は記事の翻訳に使用されます。",
"setting.frontMatter.content.i18n.markdownDescription": "ウェブサイトで利用するロケールを設定します。この設定はページフォルダーレベルの設定より優先されます。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.content.i18n)",
"setting.frontMatter.content.i18n.items.properties.title.description": "言語名",
"setting.frontMatter.content.i18n.items.properties.locale.description": "言語コード",
"setting.frontMatter.content.i18n.items.properties.path.description": "言語フォルダーの相対パス",
"setting.frontMatter.content.placeholders.markdownDescription": "記事タイプとテンプレートで使用するプレースホルダーを配列で定義して、記事のfront matterを自動で入力できるようにします。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.content.placeholders)",
"setting.frontMatter.content.placeholders.items.properties.id.description": "プレースホルダーのIDを記事タイプまたはテンプレートで、次のように使用します: {{placeholder}}",
"setting.frontMatter.content.placeholders.items.properties.value.description": "プレースホルダーの値",
@@ -92,6 +99,7 @@
"setting.frontMatter.content.sorting.items.properties.type.description": "フィールド値の型",
"setting.frontMatter.content.supportedFileTypes.markdownDescription": "Front Matterでサポートされるファイル形式を設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.content.supportedfiletypes)",
"setting.frontMatter.content.wysiwyg.markdownDescription": "What You See, Is What You GetWYSIWYGMarkdownコントロールを有効にします。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.content.wysiwyg)",
"setting.frontMatter.content.filters.markdownDescription": "ダッシュボードで利用するフィルターを設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.content.filters)",
"setting.frontMatter.custom.scripts.markdownDescription": "実行するNode.jsスクリプトのパスを指定します。現在のファイルのパスが引数として渡されます。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.custom.scripts)",
"setting.frontMatter.custom.scripts.items.properties.id.description": "スクリプトの ID。",
"setting.frontMatter.custom.scripts.items.properties.title.description": "スクリプトに付けるタイトル。ボタンのタイトルとして表示されます。",
@@ -100,8 +108,8 @@
"setting.frontMatter.custom.scripts.items.properties.bulk.description": "全ての記事ファイルに対してスクリプトを実行する",
"setting.frontMatter.custom.scripts.items.properties.output.description": "スクリプト出力を出力する場所を定義します。デフォルトは通知表示ですが、エディターパネルに表示するように指定できます。",
"setting.frontMatter.custom.scripts.items.properties.outputType.description": "エディター・パネルの出力のタイプ。たとえば、「マークダウン」に変更するために使用できます",
"setting.frontMatter.custom.scripts.items.properties.type.description": "スクリプトが使用される型",
"setting.frontMatter.custom.scripts.items.properties.command.description": "実行するスクリプトの種類",
"setting.frontMatter.custom.scripts.items.properties.type.description": "スクリプトが使用される型",
"setting.frontMatter.custom.scripts.items.properties.command.description": "実行するスクリプトの種類",
"setting.frontMatter.custom.scripts.items.properties.hidden.description": "UI からアクションを非表示にする",
"setting.frontMatter.custom.scripts.items.properties.environments.items.properties.type.description": "スクリプトを使用する必要がある環境タイプ",
"setting.frontMatter.custom.scripts.items.properties.environments.items.properties.script.description": "実行するスクリプトへのパス",
@@ -152,7 +160,18 @@
"setting.frontMatter.global.notifications.markdownDescription": "表示したい通知を設定します。既定では全ての通知が表示されます。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.global.notifications)",
"setting.frontMatter.global.disabledNotifications.markdownDescription": "これは、Front Matter CMSで無効にできる通知タイプの配列です。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.global.disablednotifications)",
"setting.frontMatter.media.defaultSorting.markdownDescription": "ダッシュボードのメディア一覧での既定の並び順を設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.media.defaultsorting)",
"setting.frontMatter.media.supportedMimeTypes.markdownDescription": "メディアファイルでサポートされるMIMEタイプを設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.media.supportedmimetypes)",
"setting.frontMatter.media.supportedMimeTypes.markdownDescription": "メディアコンテンツでサポートされるMIMEタイプを設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.media.supportedmimetypes)",
"setting.frontMatter.media.contentTypes.markdownDescription": "Front Matterで利用するメディアコンテンツのタイプを設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.media.contenttypes)",
"setting.frontMatter.media.contentTypes.items.description": "Front Matterで利用するメディアフコンテンツのタイプについての説明。",
"setting.frontMatter.media.contentTypes.items.properties.name.description": "メディアコンテンツのタイプ名",
"setting.frontMatter.media.contentTypes.items.properties.fileTypes.description": "利用可能とするメディアコンテンツのタイプの指定",
"setting.frontMatter.media.contentTypes.items.properties.fields.description": "メディアコンテンツのフィールドを設定します。",
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.title.description": "UI表示用のタイトル",
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.name.description": "フィールドの名前",
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.type.description": "フィールド値の型",
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.single.description": "単一行フィールド",
"setting.frontMatter.panel.freeform.markdownDescription": "未登録のタグ/カテゴリーをタグピッカーに入力可能にするかどうかを設定します有効にすると、後で保存するオプションが使えます。規定値はtrue。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.freeform)",
"setting.frontMatter.panel.actions.disabled.markdownDescription": "パネル内で非表示にしたいコマンドを設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.actions.disabled)",
"setting.frontMatter.preview.host.markdownDescription": "プレビュー表示に使用するホストのURLを設定します`http://localhost:1313`)。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.preview.host)",
@@ -164,11 +183,11 @@
"setting.frontMatter.taxonomy.commaSeparatedFields.items.description": "コンマ区切りの配列として使用するフィールドの名前。",
"setting.frontMatter.taxonomy.contentTypes.markdownDescription": "記事・ページ・その他で利用したい記事タイプを設定します。front matterで正しく`type`が設定されていることを確認してください。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.contenttypes)",
"setting.frontMatter.taxonomy.contentTypes.items.description": "Front Matterで使用する記事タイプを定義します。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.name.description": "フィールドの種類を定義する",
"setting.frontMatter.taxonomy.contentTypes.items.properties.name.description": "フィールドの種類を定義します。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fileType.description": "作成する記事タイプを指定します。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.description": "記事タイプのフィールドを定義する",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.description": "記事タイプのフィールドを定義します。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.description": "Front Matterで使用する記事タイプを定義します。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.type.description": "フィールドの種類を定義する",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.type.description": "フィールド値の型",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.name.description": "使用するフィールドの名前",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.title.description": "UI に表示するタイトル",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.description.description": "UI に表示する説明",
@@ -176,8 +195,8 @@
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.description": "選択肢を定義する",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.items.properties.id.description": "選択肢 ID",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.items.properties.title.description": "選択肢のタイトル",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.single.description": "単一行フィールドである",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.wysiwyg.description": "WYSIWYG フィールド (HTML 出力) である",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.single.description": "単一行フィールド",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.wysiwyg.description": "WYSIWYG フィールド (HTML 出力) ",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.multiple.description": "複数の値を選択できますか?",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.isPreviewImage.description": "画像フィールドをプレビューとして使用できるかどうかを指定します。記事タイプごとに使用できるプレビュー画像は1つだけです。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.hidden.description": "メタデータ セクションからフィールドを非表示にしますか?",
@@ -210,6 +229,7 @@
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.properties.caseSensitive.description": "比較で大文字と小文字を区別するかどうかを指定します。デフォルト: true",
"setting.frontMatter.taxonomy.contentTypes.items.properties.pageBundle.description": "新しい記事を作成するときにフォルダーを作成するかどうかを指定します。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.previewPath.description": "記事タイプのカスタム プレビュー パスを定義します。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.slugTemplate.description": "記事タイプのカスタムスラッグのテンプレートを設定します。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.template.description": "新しい記事の作成に使用できるオプションのテンプレート。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.postScript.description": "新しい記事の作成後に使用できるオプションのポストスクリプト。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.filePrefix.description": "ファイル名の接頭辞を定義します。",
@@ -236,6 +256,7 @@
"setting.frontMatter.taxonomy.seoTitleLength.markdownDescription": "SEOに適したタイトルの文字数を設定します。`-1`に設定するとオフになります。)[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.seotitlelength)",
"setting.frontMatter.taxonomy.slugPrefix.markdownDescription": "スラッグに付与する接頭辞を設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugprefix)",
"setting.frontMatter.taxonomy.slugSuffix.markdownDescription": "スラッグに付与する接尾辞を設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugsuffix)",
"setting.frontMatter.taxonomy.slugTemplate.markdownDescription": "記事を作成する際のカスタムスラッグのテンプレートを設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugtemplate)",
"setting.frontMatter.taxonomy.tags.markdownDescription": "Front Matterで利用するタグを管理します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.tags)",
"setting.frontMatter.telemetry.disable.markdownDescription": "利用状況の送信をオフにします。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.telemetry.disable)",
"setting.frontMatter.templates.enabled.markdownDescription": "テンプレート機能を利用します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.enabled)",
@@ -250,5 +271,8 @@
"command.frontMatter.settings.refresh": "Front Matterの設定の再読み込み",
"setting.frontMatter.config.dynamicFilePath.markdownDescription": "動的構成ファイルへのパスを指定します (例: [[ワークスペース]]/config.js)。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.config.dynamicfilepath)",
"setting.frontMatter.taxonomy.contentTypes.items.properties.allowAsSubContent.description": "記事をサブコンテンツとして作成可能かどうかを設定します。",
"setting.frontMatter.taxonomy.contentTypes.items.properties.isSubContent.description": "記事をサブコンテンツとして作成するかどうかを設定します。"
"setting.frontMatter.taxonomy.contentTypes.items.properties.isSubContent.description": "記事をサブコンテンツとして作成するかどうかを設定します。",
"setting.frontMatter.git.disableOnBranches.markdownDescription": "Git Actionsを無効化したいブランチを設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.git.disableonbranches)",
"setting.frontMatter.git.requiresCommitMessage.markdownDescription": "特定のブランチを更新する場合にコミットメッセージを必須にします。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.git.requirescommitmessage)"
}

View File

@@ -15,7 +15,7 @@
"command.frontMatter.initTemplate": "Initialize the template folder",
"command.frontMatter.createTemplate": "Create template from current file",
"command.frontMatter.createCategory": "Create category",
"command.frontMatter.createContent": "Create new content from defined content type or template",
"command.frontMatter.createContent": "Create new content",
"command.frontMatter.createTag": "Create tag",
"command.frontMatter.diagnostics": "Diagnostic logging",
"command.frontMatter.exportTaxonomy": "Export all tags & categories to your settings",
@@ -48,6 +48,7 @@
"command.frontMatter.markup.unorderedlist": "Unordered list",
"command.frontMatter.git.sync": "Sync",
"command.frontMatter.cache.clear": "Clear cache",
"command.frontMatter.i18n.create": "Create new translation",
"settings.configuration.title": "Front Matter: use frontmatter.json for shared team settings",
"setting.frontMatter.projects.markdownDescription": "Specify the list of projects to load in the Front Matter CMS. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.projects)",
"setting.frontMatter.projects.items.properties.name.markdownDescription": "Specify the name of the project.",
@@ -75,6 +76,12 @@
"setting.frontMatter.content.pageFolders.items.properties.filePrefix.description": "Defines a prefix for the file name.",
"setting.frontMatter.content.pageFolders.items.properties.contentTypes.description": "Defines which content types can be used for the current location. If not defined, all content types will be available.",
"setting.frontMatter.content.pageFolders.items.properties.disableCreation.description": "Disable the creation of new content in the folder.",
"setting.frontMatter.content.pageFolders.items.properties.defaultLocale.description": "Set the default locale ID for the page folder. All content from this folder is translatable to the languages defined in the `frontMatter.content.i18n` setting.",
"setting.frontMatter.content.pageFolders.items.properties.locales.description": "Define the locales for the page folder. This will be used for the translation of the content.",
"setting.frontMatter.content.i18n.markdownDescription": "Specify the locales you want to use for your website. This setting can be overwritten on page folder level. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.i18n)",
"setting.frontMatter.content.i18n.items.properties.title.description": "Title of the locale",
"setting.frontMatter.content.i18n.items.properties.locale.description": "Locale code",
"setting.frontMatter.content.i18n.items.properties.path.description": "Relative path of the locale folder",
"setting.frontMatter.content.placeholders.markdownDescription": "This array of placeholders defines the placeholders that you can use in your content types and templates for automatically populating your content its front matter. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.placeholders)",
"setting.frontMatter.content.placeholders.items.properties.id.description": "ID of the placeholder, in your content type or template, use it as follows: {{placeholder}}",
"setting.frontMatter.content.placeholders.items.properties.value.description": "The placeholder its value",
@@ -154,6 +161,17 @@
"setting.frontMatter.global.disabledNotifications.markdownDescription": "This is an array with the notifications types that can be disabled for Front Matter CMS. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.global.disablednotifications)",
"setting.frontMatter.media.defaultSorting.markdownDescription": "Specify the default sorting option for the media dashboard. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.media.defaultsorting)",
"setting.frontMatter.media.supportedMimeTypes.markdownDescription": "Specify the mime types to support for the media files. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.media.supportedmimetypes)",
"setting.frontMatter.media.contentTypes.markdownDescription": "Specify the media content types you want to use in Front Matter. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.media.contenttypes)",
"setting.frontMatter.media.contentTypes.items.description": "Define the media content types you want to use in Front Matter.",
"setting.frontMatter.media.contentTypes.items.properties.name.description": "Name of the media content type",
"setting.frontMatter.media.contentTypes.items.properties.fileTypes.description": "Specify the file types to allow for the media content type",
"setting.frontMatter.media.contentTypes.items.properties.fields.description": "Define the fields of the media content type",
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.title.description": "Title to show in the UI",
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.name.description": "Name of the field to use",
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.type.description": "Define the type of field",
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.single.description": "Is a single line field",
"setting.frontMatter.panel.freeform.markdownDescription": "Specifies if you want to allow yourself from entering unknown tags/categories in the tag picker (when enabled, you will have the option to store them afterwards). Default: true. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.freeform)",
"setting.frontMatter.panel.actions.disabled.markdownDescription": "Specify the actions you want to disable in the panel. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.actions.disabled)",
"setting.frontMatter.preview.host.markdownDescription": "Specify the host URL (example: http://localhost:1313) to be used when opening the preview. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.preview.host)",
@@ -211,6 +229,7 @@
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.properties.caseSensitive.description": "Specify if the comparison is case sensitive. Default: true",
"setting.frontMatter.taxonomy.contentTypes.items.properties.pageBundle.description": "Specify if you want to create a folder when creating new content.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.previewPath.description": "Defines a custom preview path for the content type.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.slugTemplate.description": "Defines a custom slug template for the content type.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.template.description": "An optional template that can be used for creating new content.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.postScript.description": "An optional post script that can be used after new content creation.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.filePrefix.description": "Defines a prefix for the file name.",
@@ -237,6 +256,7 @@
"setting.frontMatter.taxonomy.seoTitleLength.markdownDescription": "Specifies the optimal title length for SEO (set to `-1` to turn it off). [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.seotitlelength)",
"setting.frontMatter.taxonomy.slugPrefix.markdownDescription": "Specify a prefix for the slug. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugprefix)",
"setting.frontMatter.taxonomy.slugSuffix.markdownDescription": "Specify a suffix for the slug. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugsuffix)",
"setting.frontMatter.taxonomy.slugTemplate.markdownDescription": "Defines a custom slug template for the content you will create. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugtemplate)",
"setting.frontMatter.taxonomy.tags.markdownDescription": "Specifies the tags which can be used in the Front Matter. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.tags)",
"setting.frontMatter.telemetry.disable.markdownDescription": "Specify if you want to disable the telemetry. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.telemetry.disable)",
"setting.frontMatter.templates.enabled.markdownDescription": "Specify if you want to use templates. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.enabled)",
@@ -251,5 +271,8 @@
"command.frontMatter.settings.refresh": "Refresh Front Matter Settings",
"setting.frontMatter.config.dynamicFilePath.markdownDescription": "Specify the path to the dynamic config file (ex: [[workspace]]/config.js). [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.config.dynamicfilepath)",
"setting.frontMatter.taxonomy.contentTypes.items.properties.allowAsSubContent.description": "Specify if the content type can be used as sub content.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.isSubContent.description": "Specify if the content type is sub content."
"setting.frontMatter.taxonomy.contentTypes.items.properties.isSubContent.description": "Specify if the content type is sub content.",
"setting.frontMatter.git.disableOnBranches.markdownDescription": "Specify the branches on which you want to disable the Git actions. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.git.disableonbranches)",
"setting.frontMatter.git.requiresCommitMessage.markdownDescription": "Specify if you want to require a commit message when publishing your changes for a specified branch. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.git.requirescommitmessage)"
}

View File

@@ -1,3 +1,13 @@
import {
Position,
TextDocument,
TextDocumentWillSaveEvent,
TextEdit,
Uri,
commands,
window,
workspace
} from 'vscode';
import { Folders } from './Folders';
import { DEFAULT_CONTENT_TYPE } from './../constants/ContentType';
import { isValidFile } from './../helpers/isValidFile';
@@ -10,34 +20,57 @@ 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';
import { format } from 'date-fns';
import { ArticleHelper, Settings, SlugHelper } from '../helpers';
import {
ArticleHelper,
Settings,
SlugHelper,
processArticlePlaceholdersFromData,
processTimePlaceholders
} from '../helpers';
import { Notifications } from '../helpers/Notifications';
import { extname, basename, parse, dirname } from 'path';
import { COMMAND_NAME, DefaultFields } from '../constants';
import { DashboardData, SnippetRange } from '../models/DashboardData';
import { DashboardData, SnippetInfo, SnippetRange } from '../models/DashboardData';
import { DateHelper } from '../helpers/DateHelper';
import { parseWinPath } from '../helpers/parseWinPath';
import { Telemetry } from '../helpers/Telemetry';
import { ParsedFrontMatter } from '../parsers';
import { MediaListener } from '../listeners/panel';
import { NavigationType } from '../dashboardWebView/models';
import { processKnownPlaceholders } from '../helpers/PlaceholderHelper';
import { Position } from 'vscode';
import { SNIPPET } from '../constants/Snippet';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../localization';
export class Article {
/**
* Registers the commands for the Article class.
*
* @param subscriptions - The array of subscriptions to register the commands with.
*/
public static async registerCommands(subscriptions: unknown[]) {
subscriptions.push(
commands.registerCommand(COMMAND_NAME.setLastModifiedDate, Article.setLastModifiedDate)
);
subscriptions.push(commands.registerCommand(COMMAND_NAME.generateSlug, Article.updateSlug));
// Inserting an image in Markdown
subscriptions.push(commands.registerCommand(COMMAND_NAME.insertMedia, Article.insertMedia));
// Inserting a snippet in Markdown
subscriptions.push(commands.registerCommand(COMMAND_NAME.insertSnippet, Article.insertSnippet));
}
/**
* Sets the article date
*/
public static async setDate() {
const editor = vscode.window.activeTextEditor;
const editor = window.activeTextEditor;
if (!editor) {
return;
}
@@ -71,7 +104,7 @@ export class Article {
* Sets the article lastmod date
*/
public static async setLastModifiedDate() {
const editor = vscode.window.activeTextEditor;
const editor = window.activeTextEditor;
if (!editor) {
return;
}
@@ -85,9 +118,7 @@ export class Article {
ArticleHelper.update(editor, updatedArticle as ParsedFrontMatter);
}
public static async setLastModifiedDateOnSave(
document: vscode.TextDocument
): Promise<vscode.TextEdit[]> {
public static async setLastModifiedDateOnSave(document: TextDocument): Promise<TextEdit[]> {
const updatedArticle = this.setLastModifiedDateInner(document);
if (typeof updatedArticle === 'undefined') {
@@ -99,9 +130,7 @@ export class Article {
return [update];
}
private static setLastModifiedDateInner(
document: vscode.TextDocument
): ParsedFrontMatter | undefined {
private static setLastModifiedDateInner(document: TextDocument): ParsedFrontMatter | undefined {
const article = ArticleHelper.getFrontMatterFromDocument(document);
// Only set the date, if there is already front matter set
@@ -110,9 +139,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(
@@ -124,7 +154,7 @@ export class Article {
/**
* Generate the new slug
*/
public static generateSlug(title: string) {
public static generateSlug(title: string, article?: ParsedFrontMatter, slugTemplate?: string) {
if (!title) {
return;
}
@@ -132,13 +162,15 @@ export class Article {
const prefix = Settings.get(SETTING_SLUG_PREFIX) as string;
const suffix = Settings.get(SETTING_SLUG_SUFFIX) as string;
const slug = SlugHelper.createSlug(title);
if (article?.data) {
const slug = SlugHelper.createSlug(title, article?.data, slugTemplate);
if (slug) {
return {
slug,
slugWithPrefixAndSuffix: `${prefix}${slug}${suffix}`
};
if (slug) {
return {
slug,
slugWithPrefixAndSuffix: `${prefix}${slug}${suffix}`
};
}
}
return undefined;
@@ -151,7 +183,7 @@ export class Article {
Telemetry.send(TelemetryEvent.generateSlug);
const updateFileName = Settings.get(SETTING_SLUG_UPDATE_FILE_NAME) as string;
const editor = vscode.window.activeTextEditor;
const editor = window.activeTextEditor;
if (!editor) {
return;
@@ -168,7 +200,7 @@ export class Article {
const titleField = 'title';
const articleTitle: string = article.data[titleField];
const slugInfo = Article.generateSlug(articleTitle);
const slugInfo = Article.generateSlug(articleTitle, article, contentType.slugTemplate);
if (slugInfo && slugInfo.slug && slugInfo.slugWithPrefixAndSuffix) {
article.data['slug'] = slugInfo.slugWithPrefixAndSuffix;
@@ -192,9 +224,13 @@ export class Article {
);
for (const pField of customPlaceholderFields) {
article.data[pField.name] = customPlaceholder.value;
article.data[pField.name] = processKnownPlaceholders(
article.data[pField.name] = processArticlePlaceholdersFromData(
article.data[pField.name],
article.data,
contentType
);
article.data[pField.name] = processTimePlaceholders(
article.data[pField.name],
articleTitle,
dateFormat
);
}
@@ -206,7 +242,7 @@ export class Article {
// Check if the file name should be updated by the slug
// This is required for systems like Jekyll
if (updateFileName) {
const editor = vscode.window.activeTextEditor;
const editor = window.activeTextEditor;
if (editor) {
const ext = extname(editor.document.fileName);
const fileName = basename(editor.document.fileName);
@@ -224,7 +260,7 @@ export class Article {
try {
await editor.document.save();
await vscode.workspace.fs.rename(editor.document.uri, vscode.Uri.file(newPath), {
await workspace.fs.rename(editor.document.uri, Uri.file(newPath), {
overwrite: false
});
} catch (e: unknown) {
@@ -244,11 +280,26 @@ export class Article {
* Retrieve the slug from the front matter
*/
public static getSlug() {
const editor = vscode.window.activeTextEditor;
const editor = window.activeTextEditor;
if (!editor) {
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)) {
@@ -269,7 +320,7 @@ export class Article {
* Toggle the page its draft mode
*/
public static async toggleDraft() {
const editor = vscode.window.activeTextEditor;
const editor = window.activeTextEditor;
if (!editor) {
return;
}
@@ -287,7 +338,7 @@ export class Article {
* Article auto updater
* @param event
*/
public static async autoUpdate(event: vscode.TextDocumentWillSaveEvent) {
public static async autoUpdate(event: TextDocumentWillSaveEvent) {
const document = event.document;
if (document && ArticleHelper.isSupportedFile(document)) {
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
@@ -327,7 +378,7 @@ export class Article {
* Insert an image from the media dashboard into the article
*/
public static async insertMedia() {
const editor = vscode.window.activeTextEditor;
const editor = window.activeTextEditor;
if (!editor) {
return;
}
@@ -339,7 +390,7 @@ export class Article {
const position = editor.selection.active;
const selectionText = editor.document.getText(editor.selection);
await vscode.commands.executeCommand(COMMAND_NAME.dashboard, {
await commands.executeCommand(COMMAND_NAME.dashboard, {
type: 'media',
data: {
pageBundle: !!contentType.pageBundle,
@@ -358,7 +409,7 @@ export class Article {
* Insert a snippet into the article
*/
public static async insertSnippet() {
const editor = vscode.window.activeTextEditor;
const editor = window.activeTextEditor;
if (!editor) {
return;
}
@@ -388,7 +439,7 @@ export class Article {
snippetStartBeforePos = linesBeforeSelection.length - snippetStartBeforePos - 1;
}
let snippetInfo: { id: string; fields: any[] } | undefined = undefined;
let snippetInfo: SnippetInfo | undefined = undefined;
let range: SnippetRange | undefined = undefined;
if (
snippetEndAfterPos > -1 &&
@@ -412,13 +463,15 @@ export class Article {
}
const article = ArticleHelper.getFrontMatter(editor);
const contentType = article ? ArticleHelper.getContentType(article) : undefined;
await vscode.commands.executeCommand(COMMAND_NAME.dashboard, {
await commands.executeCommand(COMMAND_NAME.dashboard, {
type: NavigationType.Snippets,
data: {
fileTitle: article?.data.title || '',
filePath: editor.document.uri.fsPath,
fieldName: basename(editor.document.uri.fsPath),
contentType,
position,
range,
selection: selectionText,

View File

@@ -3,11 +3,13 @@ import {
CONTEXT,
ExtensionState,
SETTING_EXPERIMENTAL,
SETTING_EXTENSIBILITY_SCRIPTS
SETTING_EXTENSIBILITY_SCRIPTS,
COMMAND_NAME,
TelemetryEvent
} from '../constants';
import { join } from 'path';
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window } from 'vscode';
import { DashboardSettings, Logger, Settings as SettingsHelper } from '../helpers';
import { DashboardSettings, Logger, Settings as SettingsHelper, Telemetry } from '../helpers';
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { Extension } from '../helpers/Extension';
import { WebviewHelper } from '@estruyf/vscode';
@@ -22,7 +24,6 @@ import {
ExtensionListener,
SnippetListener,
TaxonomyListener,
LogListener,
LocalizationListener,
SsgListener
} from '../listeners/dashboard';
@@ -31,6 +32,8 @@ import { GitListener, ModeListener } from '../listeners/general';
import { Folders } from './Folders';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../localization';
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
import { NavigationType } from '../dashboardWebView/models';
export class Dashboard {
private static webview: WebviewPanel | null = null;
@@ -51,6 +54,56 @@ export class Dashboard {
}
}
public static registerCommands() {
const subscriptions = Extension.getInstance().subscriptions;
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboard, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openContentDashboard);
if (!data) {
Dashboard.open({ type: NavigationType.Contents });
} else {
Dashboard.open(data);
}
})
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboardMedia, () => {
Telemetry.send(TelemetryEvent.openMediaDashboard);
Dashboard.open({ type: NavigationType.Media });
})
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboardSnippets, () => {
Telemetry.send(TelemetryEvent.openSnippetsDashboard);
Dashboard.open({ type: NavigationType.Snippets });
})
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboardData, () => {
Telemetry.send(TelemetryEvent.openDataDashboard);
Dashboard.open({ type: NavigationType.Data });
})
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboardTaxonomy, () => {
Telemetry.send(TelemetryEvent.openTaxonomyDashboard);
Dashboard.open({ type: NavigationType.Taxonomy });
})
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboardClose, () => {
Telemetry.send(TelemetryEvent.closeDashboard);
Dashboard.close();
})
);
}
/**
* Open or reveal the dashboard
*/
@@ -183,7 +236,6 @@ export class Dashboard {
ModeListener.process(msg);
GitListener.process(msg);
TaxonomyListener.process(msg);
LogListener.process(msg);
SsgListener.process(msg);
});
}
@@ -204,7 +256,7 @@ export class Dashboard {
* @param msg
*/
public static postWebviewMessage(msg: {
command: DashboardCommand;
command: DashboardCommand | DashboardMessage;
requestId?: string;
payload?: unknown;
error?: unknown;
@@ -304,7 +356,7 @@ export class Dashboard {
version.usedVersion ? '' : `data-showWelcome="true"`
} ${
experimental ? `data-experimental="${experimental}"` : ''
} data-webview-url="${webviewUrl}" ></div>
} data-webview-url="${webviewUrl}" data-is-crash-disabled="${!Telemetry.isVscodeEnabled()}" ></div>
${(scriptsToLoad || [])
.map((script) => {

View File

@@ -1,6 +1,7 @@
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
import { Questions } from './../helpers/Questions';
import {
SETTING_CONTENT_I18N,
SETTING_CONTENT_PAGE_FOLDERS,
SETTING_CONTENT_STATIC_FOLDER,
SETTING_CONTENT_SUPPORTED_FILETYPES,
@@ -9,11 +10,11 @@ import {
} from './../constants';
import { commands, Uri, workspace, window } from 'vscode';
import { basename, dirname, join, relative, sep } from 'path';
import { ContentFolder, FileInfo, FolderInfo, StaticFolder } from '../models';
import { ContentFolder, FileInfo, FolderInfo, I18nConfig, StaticFolder } from '../models';
import uniqBy = require('lodash.uniqby');
import { Template } from './Template';
import { Notifications } from '../helpers/Notifications';
import { Logger, processKnownPlaceholders, Settings } from '../helpers';
import { Logger, Settings, processTimePlaceholders } from '../helpers';
import { existsSync } from 'fs';
import { format } from 'date-fns';
import { Dashboard } from './Dashboard';
@@ -98,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) {
@@ -288,67 +289,9 @@ export class Folders {
const folderInfo: FolderInfo[] = [];
for (const folder of folders) {
try {
const folderPath = parseWinPath(folder.path);
if (typeof folderPath === 'string') {
let files: Uri[] = [];
for (const fileType of supportedFiles || DEFAULT_FILE_TYPES) {
let filePath = join(
folderPath,
folder.excludeSubdir ? '/' : '**',
`*${fileType.startsWith('.') ? '' : '.'}${fileType}`
);
if (folderPath === '' && folder.excludeSubdir) {
filePath = `*${fileType.startsWith('.') ? '' : '.'}${fileType}`;
}
let foundFiles = await Folders.findFiles(filePath);
// Make sure these file are coming from the folder path (this could be an issue in multi-root workspaces)
foundFiles = foundFiles.filter((f) => parseWinPath(f.fsPath).startsWith(folderPath));
files = [...files, ...foundFiles];
}
if (files) {
let fileStats: FileInfo[] = [];
for (const file of files) {
try {
const fileName = basename(file.fsPath);
const folderName = dirname(file.fsPath).split(sep).pop();
const stats = await workspace.fs.stat(file);
fileStats.push({
filePath: file.fsPath,
fileName,
folderName,
...stats
});
} catch (error) {
// Skip the file
}
}
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime);
if (limit) {
fileStats = fileStats.slice(0, limit);
}
folderInfo.push({
title: folder.title,
files: files.length,
lastModified: fileStats
});
}
}
} catch (e) {
// Skip the current folder
const crntFolderInfo = await Folders.getFilesByFolder(folder, supportedFiles, limit);
if (crntFolderInfo) {
folderInfo.push(crntFolderInfo);
}
}
@@ -365,11 +308,34 @@ export class Folders {
public static get(): ContentFolder[] {
const wsFolder = Folders.getWorkspaceFolder();
let folders: ContentFolder[] = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[];
const i18nSettings = Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
// Filter out folders without a path
folders = folders.filter((f) => f.path);
const contentFolders = folders.map((folder) => {
const contentFolders: ContentFolder[] = [];
// Check if wildcard is used
const wildcardFolders = folders.filter((f) => f.path.includes('*'));
if (wildcardFolders && wildcardFolders.length > 0) {
for (const folder of wildcardFolders) {
folders = folders.filter((f) => f.path !== folder.path);
const folderPath = Folders.absWsFolder(folder, wsFolder);
const subFolders = glob.sync(folderPath, { ignore: '**/node_modules/**' });
for (const subFolder of subFolders) {
const subFolderPath = parseWinPath(subFolder);
folders.push({
...folder,
title: `${folder.title} (${subFolderPath.replace(wsFolder?.fsPath || '', '')})`,
path: subFolderPath
});
}
}
}
folders.forEach((folder) => {
if (!folder.title) {
folder.title = basename(folder.path);
}
@@ -377,7 +343,7 @@ export class Folders {
let folderPath: string | undefined = Folders.absWsFolder(folder, wsFolder);
if (folderPath.includes(`{{`) && folderPath.includes(`}}`)) {
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
folderPath = processKnownPlaceholders(folderPath, undefined, dateFormat);
folderPath = processTimePlaceholders(folderPath, dateFormat);
} else {
if (folderPath && !existsSync(folderPath)) {
Notifications.errorShowOnce(
@@ -403,11 +369,52 @@ export class Folders {
}
}
return {
...folder,
originalPath: folder.path,
path: folderPath
};
// Check i18n
if (folder.defaultLocale && (folder.locales || i18nSettings)) {
const i18nConfig =
folder.locales && folder.locales.length > 0 ? folder.locales : i18nSettings;
let defaultLocale;
let sourcePath = folderPath;
let localeFolders: ContentFolder[] = [];
if (i18nConfig && i18nConfig.length > 0) {
for (const i18n of i18nConfig) {
if (i18n.locale === folder.defaultLocale) {
defaultLocale = i18n;
} else if (i18n.locale !== folder.defaultLocale && i18n.path) {
localeFolders.push({
...folder,
title: folder.title,
originalPath: folder.path,
locale: i18n.locale,
localeTitle: i18n?.title || i18n.locale,
localeSourcePath: sourcePath,
path: parseWinPath(join(folderPath, i18n.path))
});
}
}
}
contentFolders.push({
...folder,
title: folder.title,
locale: folder.defaultLocale,
localeTitle: defaultLocale?.title || folder.defaultLocale,
originalPath: folder.path,
localeSourcePath: sourcePath,
path: parseWinPath(join(folderPath, defaultLocale?.path || ''))
});
contentFolders.push(...localeFolders);
} else {
contentFolders.push({
...folder,
locale: folder.defaultLocale,
originalPath: folder.path,
path: folderPath
});
}
});
return contentFolders.filter((folder) => folder !== null) as ContentFolder[];
@@ -418,8 +425,13 @@ export class Folders {
* @param folders
*/
public static async update(folders: ContentFolder[]) {
const originalFolders = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[];
const wsFolder = Folders.getWorkspaceFolder();
// Filter out the locale folders
folders = folders.filter((folder) => !folder.locale || folder.locale === folder.defaultLocale);
// Remove the internal FM properties
const folderDetails = folders
.map((folder) => {
const detail = {
@@ -431,6 +443,19 @@ export class Folders {
return null;
}
if (detail.locale && detail.locale === detail.defaultLocale) {
// Check if the folder was on the original list
const originalFolder = originalFolders.find((f) => f.path === folder.originalPath);
if (originalFolder && !originalFolder.locales && folder.locales) {
delete detail.locales;
}
delete detail.localeSourcePath;
delete detail.localeTitle;
}
delete detail.locale;
delete detail.originalPath;
return detail;
@@ -603,6 +628,99 @@ export class Folders {
return;
}
/**
* Retrieves the page folder that matches the given file path.
*
* @param filePath - The file path to match against the page folders.
* @returns The page folder that matches the file path, or undefined if no match is found.
*/
public static getPageFolderByFilePath(filePath: string): ContentFolder | undefined {
const folders = Folders.get();
const parsedPath = parseWinPath(filePath);
const pageFolderMatches = folders
.filter((folder) => parsedPath && folder.path && parsedPath.includes(folder.path))
.sort((a, b) => b.path.length - a.path.length);
if (pageFolderMatches.length > 0 && pageFolderMatches[0]) {
return pageFolderMatches[0];
}
return;
}
private static async getFilesByFolder(
folder: ContentFolder,
supportedFiles: string[] | undefined,
limit?: number
): Promise<FolderInfo | undefined> {
try {
const folderPath = parseWinPath(folder.path);
if (typeof folderPath === 'string') {
let files: Uri[] = [];
for (const fileType of supportedFiles || DEFAULT_FILE_TYPES) {
let filePath = join(
folderPath,
folder.excludeSubdir ? '/' : '**',
`*${fileType.startsWith('.') ? '' : '.'}${fileType}`
);
if (folderPath === '' && folder.excludeSubdir) {
filePath = `*${fileType.startsWith('.') ? '' : '.'}${fileType}`;
}
let foundFiles = await Folders.findFiles(filePath);
// Make sure these file are coming from the folder path (this could be an issue in multi-root workspaces)
foundFiles = foundFiles.filter((f) => parseWinPath(f.fsPath).startsWith(folderPath));
files = [...files, ...foundFiles];
}
if (files) {
let fileStats: FileInfo[] = [];
for (const file of files) {
try {
const fileName = basename(file.fsPath);
const folderName = dirname(file.fsPath).split(sep).pop();
const stats = await workspace.fs.stat(file);
fileStats.push({
filePath: file.fsPath,
fileName,
folderName,
...stats
});
} catch (error) {
// Skip the file
}
}
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime);
if (limit) {
fileStats = fileStats.slice(0, limit);
}
return {
title: folder.title,
files: files.length,
lastModified: fileStats,
locale: folder.locale,
localeTitle: folder.localeTitle
};
}
}
} catch (e) {
// Skip the current folder
}
return;
}
/**
* Retrieve all content folders
* @param pattern

View File

@@ -14,7 +14,7 @@ import {
import { ArticleHelper } from './../helpers/ArticleHelper';
import { join, parse } from 'path';
import { commands, env, Uri, ViewColumn, window, WebviewPanel, extensions } from 'vscode';
import { Extension, parseWinPath, processKnownPlaceholders, Settings } from '../helpers';
import { Extension, parseWinPath, processTimePlaceholders, Settings } from '../helpers';
import { ContentFolder, ContentType, PreviewSettings } from '../models';
import { format } from 'date-fns';
import { DateHelper } from '../helpers/DateHelper';
@@ -294,7 +294,7 @@ export class Preview {
if (pathname) {
// Known placeholders
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
pathname = processKnownPlaceholders(pathname, article?.data?.title, dateFormat);
pathname = processTimePlaceholders(pathname, dateFormat);
// Custom placeholders
pathname = await ArticleHelper.processCustomPlaceholders(
@@ -318,7 +318,7 @@ export class Preview {
}
// Support front matter placeholders - {{fm.<field>}}
pathname = processFmPlaceholders(pathname, article?.data);
pathname = article?.data ? processFmPlaceholders(pathname, article?.data) : pathname;
try {
const articleDate = ArticleHelper.getDate(article);

View File

@@ -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
});

View File

@@ -19,6 +19,7 @@ import { Field } from '../models';
import { Preview } from './Preview';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../localization';
import { i18n } from './i18n';
export class StatusListener {
/**
@@ -42,6 +43,10 @@ export class StatusListener {
try {
commands.executeCommand('setContext', CONTEXT.isValidFile, true);
// Check i18n
const isI18nEnabled = await i18n.isLocaleEnabled(document.uri.fsPath);
commands.executeCommand('setContext', CONTEXT.isI18nEnabled, isI18nEnabled);
const article = editor
? ArticleHelper.getFrontMatter(editor)
: await ArticleHelper.getFrontMatterByPath(document.uri.fsPath);
@@ -83,6 +88,7 @@ export class StatusListener {
}
} else {
commands.executeCommand('setContext', CONTEXT.isValidFile, false);
commands.executeCommand('setContext', CONTEXT.isI18nEnabled, false);
const panel = PanelProvider.getInstance();
if (panel && panel.visible) {

564
src/commands/i18n.ts Normal file
View File

@@ -0,0 +1,564 @@
import { ProgressLocation, Uri, commands, window, workspace } from 'vscode';
import {
ArticleHelper,
ContentType,
Extension,
FrameworkDetector,
Notifications,
Settings,
openFileInEditor,
parseWinPath
} from '../helpers';
import { COMMAND_NAME, SETTING_CONTENT_I18N } from '../constants';
import { ContentFolder, Field, I18nConfig, ContentType as IContentType } from '../models';
import { join, parse } from 'path';
import { existsAsync } from '../utils';
import { Folders } from '.';
import { ParsedFrontMatter } from '../parsers';
import { PagesListener } from '../listeners/dashboard';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../localization';
import { Translations } from '../services/Translations';
export class i18n {
private static processedFiles: {
[filePath: string]: { dir: string; filename: string; isPageBundle: boolean };
} = {};
/**
* Registers the i18n commands.
*/
public static register() {
const subscriptions = Extension.getInstance().subscriptions;
subscriptions.push(commands.registerCommand(COMMAND_NAME.i18n.create, i18n.create));
i18n.clearFiles();
}
/**
* Clear the processed files
*/
public static clearFiles() {
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.
*/
public static async getSettings(filePath: string): Promise<I18nConfig[] | undefined> {
if (!filePath) {
return;
}
const i18nSettings = Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
let pageFolder = Folders.getPageFolderByFilePath(filePath);
if (!pageFolder) {
pageFolder = await i18n.getPageFolder(filePath);
}
if (!pageFolder || !pageFolder.locales) {
return i18nSettings;
}
return pageFolder.locales;
}
/**
* Checks if the locale is enabled for the given file path.
* @param filePath - The file path to check.
* @returns A promise that resolves to a boolean indicating whether the locale is enabled or not.
*/
public static async isLocaleEnabled(filePath: string): Promise<boolean> {
const i18nSettings = await i18n.getSettings(filePath);
if (!i18nSettings) {
return false;
}
const pageFolder = Folders.getPageFolderByFilePath(filePath);
if (!pageFolder || !pageFolder.locale) {
return false;
}
return i18nSettings.some((i18n) => i18n.locale === pageFolder.locale);
}
/**
* Checks if the given file path corresponds to the default language.
* @param filePath - The file path to check.
* @returns True if the file path corresponds to the default language, false otherwise.
*/
public static async isDefaultLanguage(filePath: string): Promise<boolean> {
const i18nSettings = await i18n.getSettings(filePath);
if (!i18nSettings) {
return false;
}
const pageFolder = Folders.getPageFolderByFilePath(filePath);
if (!pageFolder || !pageFolder.defaultLocale) {
return false;
}
const fileInfo = await i18n.getFileInfo(filePath);
if (pageFolder.path) {
if (pageFolder.locale) {
return pageFolder.locale === pageFolder.defaultLocale;
}
let pageFolderPath = parseWinPath(pageFolder.path);
if (!pageFolderPath.endsWith('/')) {
pageFolderPath += '/';
}
return (
parseWinPath(fileInfo.dir).toLowerCase() === parseWinPath(pageFolderPath).toLowerCase()
);
}
return false;
}
/**
* Retrieves the I18nConfig for a given file path.
* @param filePath - The path of the file.
* @returns The I18nConfig object if found, otherwise undefined.
*/
public static async getLocale(filePath: string): Promise<I18nConfig | undefined> {
const i18nSettings = await i18n.getSettings(filePath);
if (!i18nSettings) {
return;
}
let pageFolder = Folders.getPageFolderByFilePath(filePath);
const fileInfo = await i18n.getFileInfo(filePath);
if (pageFolder && pageFolder.defaultLocale) {
let pageFolderPath = parseWinPath(pageFolder.path);
if (!pageFolderPath.endsWith('/')) {
pageFolderPath += '/';
}
if (
pageFolder.path &&
pageFolder.locale &&
parseWinPath(fileInfo.dir).toLowerCase() === parseWinPath(pageFolderPath).toLowerCase()
) {
return i18nSettings.find((i18n) => i18n.locale === pageFolder?.locale);
}
}
pageFolder = await i18n.getPageFolder(filePath);
if (!pageFolder) {
return;
}
for (const locale of i18nSettings) {
if (locale.path && pageFolder.defaultLocale !== locale.locale) {
const translation = join(pageFolder.path, locale.path, fileInfo.filename);
if (parseWinPath(translation).toLowerCase() === parseWinPath(filePath).toLowerCase()) {
return locale;
}
}
}
return;
}
/**
* Retrieves translations for a given file path.
* @param filePath - The path of the file for which translations are requested.
* @returns A promise that resolves to an object containing translations for each locale, or undefined if i18n settings are not available.
*/
public static async getTranslations(filePath: string): Promise<
| {
[locale: string]: {
locale: I18nConfig;
path: string;
};
}
| undefined
> {
const i18nSettings = await i18n.getSettings(filePath);
if (!i18nSettings) {
return;
}
const translations: {
[locale: string]: {
locale: I18nConfig;
path: string;
};
} = {};
let pageFolder = Folders.getPageFolderByFilePath(filePath);
const fileInfo = await i18n.getFileInfo(filePath);
if (pageFolder && pageFolder.defaultLocale && pageFolder.localeSourcePath) {
for (const i18n of i18nSettings) {
const translation = join(pageFolder.localeSourcePath, i18n.path || '', fileInfo.filename);
if (await existsAsync(translation)) {
translations[i18n.locale] = {
locale: i18n,
path: translation
};
}
}
return translations;
}
pageFolder = await i18n.getPageFolder(filePath);
if (!pageFolder) {
return translations;
}
for (const i18n of i18nSettings) {
const translation = join(pageFolder.path, i18n.path || '', fileInfo.filename);
if (await existsAsync(translation)) {
translations[i18n.locale] = {
locale: i18n,
path: translation
};
}
}
return translations;
}
/**
* Creates a new content file for a specific locale based on the i18n configuration.
* If a file path is provided, the new content file will be created in the same directory.
* If no file path is provided, the active file in the editor will be used.
* @param filePath The path of the file where the new content file should be created.
*/
private static async create(fileUri?: Uri | string) {
if (!fileUri) {
const filePath = ArticleHelper.getActiveFile();
fileUri = filePath ? Uri.file(filePath) : undefined;
}
if (!fileUri) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoFileSelected));
return;
}
if (typeof fileUri === 'string') {
fileUri = Uri.file(fileUri);
}
const pageFolder = Folders.getPageFolderByFilePath(fileUri.fsPath);
if (!pageFolder || !pageFolder.localeSourcePath) {
Notifications.error(l10n.t(LocalizationKey.commandsI18nCreateErrorNoContentFolder));
return;
}
const i18nSettings = await i18n.getSettings(fileUri.fsPath);
if (!i18nSettings) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
return;
}
const sourceLocale = await i18n.getLocale(fileUri.fsPath);
if (!sourceLocale || !sourceLocale.locale) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocaleDefinition));
return;
}
const translations = (await i18n.getTranslations(fileUri.fsPath)) || {};
const targetLocales = i18nSettings.filter((i18nSetting) => {
return (
i18nSetting.path &&
i18nSetting.locale !== sourceLocale.locale &&
!translations[i18nSetting.locale]
);
});
if (targetLocales.length === 0) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocales));
return;
}
const locale = await window.showQuickPick(
targetLocales.map((i18n) => i18n.title || i18n.locale),
{
title: l10n.t(LocalizationKey.commandsI18nCreateQuickPickTitle),
placeHolder: l10n.t(LocalizationKey.commandsI18nCreateQuickPickPlaceHolder),
ignoreFocusOut: true
}
);
if (!locale) {
return;
}
const targetLocale = i18nSettings.find(
(i18n) => i18n.title === locale || i18n.locale === locale
);
if (!targetLocale || !targetLocale.path) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
return;
}
let article = await ArticleHelper.getFrontMatterByPath(fileUri.fsPath);
if (!article) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoFile));
return;
}
const contentType = ArticleHelper.getContentType(article);
if (!contentType) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoContentType));
return;
}
// Get the directory of the file
const fileInfo = parse(fileUri.fsPath);
let dir = fileInfo.dir;
let pageBundleDir = '';
if (await ArticleHelper.isPageBundle(fileUri.fsPath)) {
dir = ArticleHelper.getPageFolderFromBundlePath(fileUri.fsPath);
pageBundleDir = fileUri.fsPath.replace(dir, '');
pageBundleDir = join(parse(pageBundleDir).dir);
}
const i18nDir = join(pageFolder.localeSourcePath, targetLocale.path, pageBundleDir);
if (!(await existsAsync(i18nDir))) {
await workspace.fs.createDirectory(Uri.file(i18nDir));
}
article = await i18n.updateFrontMatter(
article,
fileUri.fsPath,
contentType,
sourceLocale,
targetLocale,
i18nDir
);
const newFilePath = join(i18nDir, fileInfo.base);
if (await existsAsync(newFilePath)) {
Notifications.error(l10n.t(LocalizationKey.commandsI18nCreateErrorFileExists));
return;
}
if (sourceLocale?.locale) {
article = await i18n.translate(article, sourceLocale, targetLocale);
}
const newFileUri = Uri.file(newFilePath);
await workspace.fs.writeFile(
newFileUri,
Buffer.from(ArticleHelper.stringifyFrontMatter(article.content, article.data))
);
await openFileInEditor(newFilePath);
PagesListener.refresh();
Notifications.info(
l10n.t(
LocalizationKey.commandsI18nCreateSuccessCreated,
sourceLocale.title || sourceLocale.locale
)
);
}
/**
* Translates the given article from the source locale to the target locale using DeepL translation service.
* @param article - The article to be translated.
* @param sourceLocale - The source locale configuration.
* @param targetLocale - The target locale configuration.
* @returns A promise that resolves to the translated article.
*/
private static async translate(
article: ParsedFrontMatter,
sourceLocale: I18nConfig,
targetLocale: I18nConfig
) {
return new Promise<ParsedFrontMatter>(async (resolve) => {
await window.withProgress(
{
location: ProgressLocation.Notification,
title: l10n.t(LocalizationKey.commandsI18nTranslateProgressTitle),
cancellable: false
},
async () => {
try {
const title = article.data.title || '';
const description = article.data.description || '';
const content = article.content || '';
const text = [title, description, content];
const translations = await Translations.translate(
text,
sourceLocale.locale,
targetLocale.locale
);
if (!translations || translations.length < 3) {
resolve(article);
return;
}
article.data.title = article.data.title ? translations[0] : '';
article.data.description = article.data.description ? translations[1] : '';
article.content = article.content ? translations[2] : '';
} catch (error) {
Notifications.error(`${(error as Error).message}`);
}
resolve(article);
}
);
});
}
/**
* Retrieves the filename and directory information from the given file path.
* If the file is a page bundle, the directory will be adjusted accordingly.
* @param filePath - The path of the file.
* @returns An object containing the filename and directory.
*/
private static async getFileInfo(filePath: string): Promise<{ filename: string; dir: string }> {
if (i18n.processedFiles[filePath]) {
return i18n.processedFiles[filePath];
}
const fileInfo = parse(filePath);
let filename = fileInfo.base;
let dir = fileInfo.dir;
const isPageBundle = await ArticleHelper.isPageBundle(filePath);
if (isPageBundle) {
dir = ArticleHelper.getPageFolderFromBundlePath(filePath);
filename = join(parseWinPath(filePath).replace(parseWinPath(dir), ''));
}
if (!dir.endsWith('/')) {
dir += '/';
}
i18n.processedFiles[filePath] = {
isPageBundle,
filename,
dir
};
return i18n.processedFiles[filePath];
}
/**
* Retrieves the page folder for a given file path.
*
* @param filePath - The path of the file.
* @returns A promise that resolves to the ContentFolder object representing the page folder, or undefined if not found.
*/
private static async getPageFolder(filePath: string): Promise<ContentFolder | undefined> {
const folders = Folders.get();
const localeFolders = folders?.filter((folder) => folder.defaultLocale);
if (!localeFolders) {
return;
}
const fileInfo = await i18n.getFileInfo(filePath);
for (const folder of localeFolders) {
const defaultFile = join(folder.path, fileInfo.filename);
if (await existsAsync(defaultFile)) {
return folder;
}
}
}
/**
* Updates the front matter of an article with internationalization (i18n) support.
*
* @param article - The parsed front matter of the article.
* @param filePath - The path of the file containing the front matter.
* @param contentType - The content type of the article.
* @param sourceLocale - The source locale.
* @param targetLocale - The target locale.
* @param i18nDir - The directory where the i18n files are located.
* @returns A Promise that resolves to the updated parsed front matter.
*/
private static async updateFrontMatter(
article: ParsedFrontMatter,
filePath: string,
contentType: IContentType,
sourceLocale: I18nConfig,
targetLocale: I18nConfig,
i18nDir: string
): Promise<ParsedFrontMatter> {
const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image');
if (imageFields.length > 0) {
article.data = await i18n.processImageFields(article.data, filePath, imageFields, i18nDir);
}
return article;
}
/**
* Processes the image fields in the provided data object.
* Replaces the image field values with the relative path to the image file.
*
* @param data - The data object containing the field values.
* @param filePath - The absolute file path of the data object.
* @param fields - The array of field arrays to process.
* @param i18nDir - The directory path for internationalization.
* @returns The updated data object with image field values replaced by relative paths.
*/
private static async processImageFields(
data: { [key: string]: any },
filePath: string,
fields: Field[][],
i18nDir: string
) {
for (const field of fields) {
if (!field) {
continue;
}
for (const f of field) {
if (f.type === 'image') {
const value = data[f.name];
if (value) {
let imgPath = FrameworkDetector.getAbsPathByFile(value, filePath);
imgPath = FrameworkDetector.getRelPathByFileDir(imgPath, i18nDir);
data[f.name] = imgPath;
}
}
}
}
return data;
}
}

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
export interface IArrowClockwiseIconProps {
className?: string;
}
export const ArrowClockwiseIcon: React.FunctionComponent<IArrowClockwiseIconProps> = ({ className }: React.PropsWithChildren<IArrowClockwiseIconProps>) => {
return (
<svg className={className || 'h-4 w-4'} fill="currentColor" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M3.07 9.05a7 7 0 0 1 12.55-3.22l.13.17H12.5a.5.5 0 1 0 0 1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 0-1 0v2.2a8 8 0 1 0 1.99 4.77.5.5 0 0 0-1 .08 7 7 0 1 1-13.92-.5Z" fill="currentColor"></path>
</svg>
);
};

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
export interface IRenameIconProps {
className: string;
}
export const RenameIcon: React.FunctionComponent<IRenameIconProps> = ({
className
}: React.PropsWithChildren<IRenameIconProps>) => {
return (
<svg className={className} fill="currentColor" aria-hidden="true" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M8.5 2a.5.5 0 0 0 0 1h1v14h-1a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-1V3h1a.5.5 0 0 0 0-1h-3Zm-4 2h4v1h-4C3.67 5 3 5.67 3 6.5v7c0 .83.67 1.5 1.5 1.5h4v1h-4A2.5 2.5 0 0 1 2 13.5v-7A2.5 2.5 0 0 1 4.5 4Zm11 11h-4v1h4a2.5 2.5 0 0 0 2.5-2.5v-7A2.5 2.5 0 0 0 15.5 4h-4v1h4c.83 0 1.5.67 1.5 1.5v7c0 .83-.67 1.5-1.5 1.5Z" fill="currentColor"></path></svg>
);
};

View File

@@ -0,0 +1,149 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { cn } from "../../utils/cn"
import { ChevronRightIcon } from "@heroicons/react/24/outline"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-[var(--vscode-list-hoverBackground)] data-[state=open]:bg-[var(--vscode-list-hoverBackground)]",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded border border-[var(--frontmatter-border)] bg-[var(--vscode-sideBar-background)] p-1 text-[var(--vscode-editor-foreground)] shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] rounded border border-[var(--frontmatter-border)] bg-[var(--vscode-sideBar-background)] p-1 text-[var(--vscode-editor-foreground)] shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-96 overflow-auto",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-[var(--vscode-list-hoverBackground)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50 cursor-pointer disabled:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-[var(--frontmatter-border)]", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
}

View File

@@ -67,6 +67,11 @@ export const COMMAND_NAME = {
addMissingFields: getCommandName('contenttype.addMissingFields'),
setContentType: getCommandName('contenttype.setContentType'),
// i18n
i18n: {
create: getCommandName('i18n.create')
},
// Project
switchProject: getCommandName('project.switch'),

View File

@@ -30,5 +30,15 @@ export const ExtensionState = {
v7_0_0: {
dateFields: `frontMatter:Updates:v7.0.0:dateFields`
}
},
Secrets: {
Deepl: {
ApiKey: `frontMatter:Secrets:DeeplApiKey`
},
Azure: {
TranslatorKey: `frontMatter:Secrets:AzureTranslatorKey`,
TranslatorRegion: `frontMatter:Secrets:AzureTranslatorRegion`
}
}
};

View File

@@ -1,14 +1,35 @@
export const GeneralCommands = {
toWebview: {
setMode: 'setMode',
gitSyncingStart: 'gitSyncingStart',
gitSyncingEnd: 'gitSyncingEnd',
git: {
syncingStart: 'gitSyncingStart',
syncingEnd: 'gitSyncingEnd',
branchName: 'gitBranchName'
},
setLocalization: 'setLocalization'
},
toVSCode: {
openLink: 'openLink',
gitSync: 'gitSync',
gitIsRepo: 'gitIsRepo',
git: {
isRepo: 'gitIsRepo',
sync: 'gitSync',
fetch: 'getFetch',
getBranch: 'getBranch',
selectBranch: 'gitSelectBranch'
},
secrets: {
get: 'getSecret',
set: 'setSecret'
},
content: {
locales: 'getContentLocales'
},
logging: {
info: 'logInfo',
warn: 'logWarn',
error: 'logError'
},
runCommand: 'runCommand',
getLocalization: 'getLocalization',
openOnWebsite: 'openOnWebsite'
}

View File

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

View File

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

View File

@@ -49,5 +49,6 @@ export const TelemetryEvent = {
webviewTaxonomyDashboard: 'webviewTaxonomyDashboard',
// Git
gitSync: 'gitSync'
gitSync: 'gitSync',
gitFetch: 'gitFetch'
};

View File

@@ -8,6 +8,8 @@ export const CONTEXT = {
isValidFile: 'frontMatter:file:isValid',
isDevelopment: 'frontMatter:isDevelopment',
isI18nEnabled: 'frontMatter:i18n:enabled',
hasViewModes: 'frontMatter:has:modes',
isSnippetsDashboardEnabled: 'frontMatter:dashboard:snippets:enabled',

View File

@@ -25,6 +25,7 @@ export const SETTING_TAXONOMY_CONTENT_TYPES = 'taxonomy.contentTypes';
export const SETTING_SLUG_PREFIX = 'taxonomy.slugPrefix';
export const SETTING_SLUG_SUFFIX = 'taxonomy.slugSuffix';
export const SETTING_SLUG_TEMPLATE = 'taxonomy.slugTemplate';
export const SETTING_SLUG_UPDATE_FILE_NAME = 'taxonomy.alignFilename';
export const SETTING_INDENT_ARRAY = 'taxonomy.indentArrays';
@@ -56,6 +57,7 @@ export const SETTING_CUSTOM_SCRIPTS = 'custom.scripts';
export const SETTING_AUTO_UPDATE_DATE = 'content.autoUpdateDate';
export const SETTING_CONTENT_PAGE_FOLDERS = 'content.pageFolders';
export const SETTING_CONTENT_I18N = 'content.i18n';
export const SETTING_CONTENT_STATIC_FOLDER = 'content.publicFolder';
export const SETTING_CONTENT_FRONTMATTER_HIGHLIGHT = 'content.fmHighlight';
export const SETTING_CONTENT_DRAFT_FIELD = 'content.draftField';
@@ -75,6 +77,7 @@ export const SETTING_CONTENT_HIDE_FRONTMATTER = 'content.hideFm';
export const SETTING_CONTENT_HIDE_FRONTMATTER_MESSAGE = 'content.hideFmMessage';
export const SETTING_MEDIA_SUPPORTED_MIMETYPES = 'media.supportedMimeTypes';
export const SETTING_MEDIA_CONTENTTYPES = 'media.contentTypes';
export const SETTING_DASHBOARD_OPENONSTART = 'dashboard.openOnStart';
export const SETTING_DASHBOARD_CONTENT_TAGS = 'dashboard.content.cardTags';
@@ -99,6 +102,8 @@ export const SETTING_FRAMEWORK_START = 'framework.startCommand';
export const SETTING_SITE_BASEURL = 'site.baseURL';
export const SETTING_GIT_ENABLED = 'git.enabled';
export const SETTING_GIT_DISABLED_BRANCHES = 'git.disableOnBranches';
export const SETTING_GIT_REQUIRES_COMMIT_MSG = 'git.requiresCommitMessage';
export const SETTING_GIT_COMMIT_MSG = 'git.commitMessage';
export const SETTING_GIT_SUBMODULE_PULL = 'git.submodule.pull';
export const SETTING_GIT_SUBMODULE_PUSH = 'git.submodule.push';
@@ -128,13 +133,3 @@ export const SETTING_CONTENT_FOLDERS = 'content.folders';
* Use the `isPublishDate` property on the content type datetime field instead
*/
export const SETTING_DATE_FIELD = 'taxonomy.dateField';
/**
* @deprecated
* Use the `isModifiedDate` property on the content type datetime field instead
*/
export const SETTING_MODIFIED_FIELD = 'taxonomy.modifiedField';
/**
* @deprecated
* Use the `frontMatter.content.snippets` setting instead
*/
export const SETTING_DASHBOARD_MEDIA_SNIPPET = 'dashboard.mediaSnippet';

View File

@@ -30,6 +30,7 @@ export enum DashboardMessage {
getPinnedItems = 'getPinnedItems',
pinItem = 'pinItem',
unpinItem = 'unpinItem',
rename = 'rename',
// Media Dashboard
getMedia = 'getMedia',
@@ -54,6 +55,7 @@ export enum DashboardMessage {
insertSnippet = 'insertSnippet',
addSnippet = 'addSnippet',
updateSnippet = 'updateSnippet',
updateSnippetPlaceholders = 'updateSnippetPlaceholders',
// Taxonomy dashboard
getTaxonomyData = 'getTaxonomyData',
@@ -73,7 +75,6 @@ export enum DashboardMessage {
getState = 'getState',
runCustomScript = 'runCustomScript',
sendTelemetry = 'sendTelemetry',
logError = 'logError',
showNotification = 'showNotification',
// Settings

View File

@@ -9,7 +9,7 @@ import { Contents } from './Contents/Contents';
import { Media } from './Media/Media';
import { DataView } from './DataView';
import { Snippets } from './SnippetsView/Snippets';
import { FEATURE_FLAG } from '../../constants';
import { FEATURE_FLAG, GeneralCommands } from '../../constants';
import { Messenger } from '@estruyf/vscode/dist/client';
import { TaxonomyView } from './TaxonomyView';
import { Route, Routes, useNavigate } from 'react-router-dom';
@@ -18,7 +18,6 @@ import { useEffect, useMemo, useState } from 'react';
import { UnknownView } from './UnknownView';
import { ErrorBoundary } from '@sentry/react';
import { ErrorView } from './ErrorView';
import { DashboardMessage } from '../DashboardMessage';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { SettingsView } from './SettingsView/SettingsView';
@@ -30,7 +29,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);
@@ -55,6 +54,21 @@ export const App: React.FunctionComponent<IAppProps> = ({
return isAllowed(mode?.features || [], FEATURE_FLAG.dashboard.taxonomy.view);
}, [mode?.features]);
const checkDevMode = (retry: number = 0) => {
if (!window.fmExternal) {
if (retry < 5) {
setTimeout(() => checkDevMode(retry + 1), 150);
} else {
setIsDevMode(false);
return;
}
}
if (window.fmExternal && window.fmExternal.isDevelopment) {
setIsDevMode(true);
}
}
useEffect(() => {
if (view && routePaths[view]) {
navigate(routePaths[view]);
@@ -65,12 +79,10 @@ export const App: React.FunctionComponent<IAppProps> = ({
}, [view]);
useEffect(() => {
if (window.fmExternal && window.fmExternal.isDevelopment) {
setIsDevMode(true);
}
checkDevMode();
}, []);
if (!settings || !localeReady) {
if (!settings) {
return <Spinner />;
}
@@ -87,7 +99,7 @@ export const App: React.FunctionComponent<IAppProps> = ({
fallback={<ErrorView />}
onError={(error: Error, componentStack: string, eventId: string) => {
Messenger.send(
DashboardMessage.logError,
GeneralCommands.toVSCode.logging.error,
`Event ID: ${eventId}
Message: ${error.message}

View File

@@ -1,9 +1,9 @@
import { Menu } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { MenuItem, MenuItems } from '../Menu';
import { MenuItem } from '../Menu';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
export interface IChoiceButtonProps {
title: string;
@@ -36,40 +36,39 @@ export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({
{title}
</button>
{choices.length > 0 && (
<Menu as="span" className="-ml-px relative block">
<Menu.Button
className={`h-full inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium focus:outline-none rounded-r text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50`}
disabled={disabled}
>
<span className="sr-only">{l10n.t(LocalizationKey.dashboardCommonChoiceButtonOpen)}</span>
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
<MenuItems widthClass={`w-56`} disablePopper>
<div className="py-1">
{choices.map((choice, idx) => (
<MenuItem
key={idx}
title={
choice.icon ? (
<div className="flex items-center">
{choice.icon}
<span>{choice.title}</span>
</div>
) : (
choice.title
)
}
value={null}
onClick={choice.onClick}
disabled={choice.disabled}
/>
))}
</div>
</MenuItems>
</Menu>
)}
</span>
{choices.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger
className='h-full inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium focus:outline-none rounded-r text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50'
disabled={disabled}>
<span className="sr-only">{l10n.t(LocalizationKey.dashboardCommonChoiceButtonOpen)}</span>
<ChevronDownIcon className={`h-4 w-4`} aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
{choices.map((choice, idx) => (
<MenuItem
key={idx}
title={
choice.icon ? (
<div className="flex items-center">
{choice.icon}
<span>{choice.title}</span>
</div>
) : (
choice.title
)
}
value={null}
onClick={choice.onClick}
disabled={choice.disabled}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
</span >
);
};

View File

@@ -36,7 +36,7 @@ export const DateField: React.FunctionComponent<IDateFieldProps> = ({
}
return (
<span className={`date__field ${className || ''} text-xs text-[var(--frontmatter-text)]`}>
<span className={`date__field ${className || ''} text-xs text-[var(--frontmatter-secondary-text)]`}>
{dateValue}
</span>
);

View File

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

View File

@@ -20,7 +20,7 @@ export const Spinner: React.FunctionComponent<ISpinnerProps> = (
{
type === 'initPages' && (
<div className='spinner-msg h-full text-2xl flex justify-center items-center text-[var(--vscode-foreground)]'>
<div className='spinner-msg h-full text-2xl flex justify-center items-center text-[var(--frontmatter-text)]'>
<span>{l10n.t(LocalizationKey.loadingInitPages)}</span>
<span className='dots'></span>
</div>

View File

@@ -1,230 +1,170 @@
import { Messenger, messageHandler } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import { EyeIcon, GlobeEuropeAfricaIcon, CommandLineIcon, TrashIcon } from '@heroicons/react/24/outline';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { EyeIcon, GlobeEuropeAfricaIcon, TrashIcon, LanguageIcon, EllipsisHorizontalIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { CustomScript, ScriptType } from '../../../models';
import { CustomScript, I18nConfig } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { MenuItem, MenuItems, ActionMenuButton, QuickAction } from '../Menu';
import { Alert } from '../Modals/Alert';
import { usePopper } from 'react-popper';
import { useState } from 'react';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { useRecoilState, useRecoilValue } from 'recoil';
import { SettingsSelector } from '../../state';
import { GeneralCommands } from '../../../constants';
import { SelectedItemActionAtom, SettingsSelector } from '../../state';
import { COMMAND_NAME, GeneralCommands } from '../../../constants';
import { PinIcon } from '../Icons/PinIcon';
import { PinnedItemsAtom } from '../../state/atom/PinnedItems';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { RenameIcon } from '../../../components/icons/RenameIcon';
import { openOnWebsite } from '../../utils';
import { CustomActions } from './CustomActions';
import { TranslationMenu } from './TranslationMenu';
export interface IContentActionsProps {
title: string;
path: string;
relPath: string;
contentType: string;
scripts: CustomScript[] | undefined;
listView?: boolean;
locale?: I18nConfig;
isDefaultLocale?: boolean;
translations?: {
[locale: string]: {
locale: I18nConfig;
path: string;
};
};
onOpen: () => void;
}
export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
title,
path,
relPath,
contentType,
scripts,
onOpen,
listView
listView,
isDefaultLocale,
translations,
locale
}: React.PropsWithChildren<IContentActionsProps>) => {
const [, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
const [pinnedItems, setPinnedItems] = useRecoilState(PinnedItemsAtom);
const [showDeletionAlert, setShowDeletionAlert] = React.useState(false);
const settings = useRecoilValue(SettingsSelector);
const [referenceElement, setReferenceElement] = useState<any>(null);
const [popperElement, setPopperElement] = useState<any>(null);
const { styles, attributes, forceUpdate } = usePopper(referenceElement, popperElement, {
placement: listView ? 'right-start' : 'bottom-end',
strategy: 'fixed'
});
const onView = (e: React.MouseEvent<HTMLButtonElement>) => {
const onView = (e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
onOpen();
};
const onDelete = (e: React.MouseEvent<HTMLButtonElement>) => {
const onDelete = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
setShowDeletionAlert(true);
};
setSelectedItemAction({ path, action: 'delete' });
}, [path]);
const onDeleteConfirm = () => {
if (path) {
Messenger.send(DashboardMessage.deleteFile, path);
}
setShowDeletionAlert(false);
};
const openOnWebsite = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
const onRename = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
if (settings?.websiteUrl && path) {
Messenger.send(GeneralCommands.toVSCode.openOnWebsite, {
websiteUrl: settings.websiteUrl,
filePath: path
});
}
messageHandler.send(DashboardMessage.rename, path);
}, [path])
const onOpenWebsite = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
openOnWebsite(settings?.websiteUrl, path);
}, [settings?.websiteUrl, path]);
const pinItem = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
const pinItem = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
messageHandler.request<string[]>(DashboardMessage.pinItem, path).then((result) => {
setPinnedItems(result || []);
})
}, [path]);
const unpinItem = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
const unpinItem = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
messageHandler.request<string[]>(DashboardMessage.unpinItem, path).then((result) => {
setPinnedItems(result || []);
})
}, [path]);
const runCustomScript = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement>, script: CustomScript) => {
e.stopPropagation();
Messenger.send(DashboardMessage.runCustomScript, { script, path });
},
[path]
);
const runCommand = React.useCallback((commandId: string) => {
messageHandler.send(GeneralCommands.toVSCode.runCommand, {
command: commandId,
args: path
})
}, [path]);
const isPinned = React.useMemo(() => {
return pinnedItems.includes(relPath);
}, [pinnedItems, relPath]);
const customScriptActions = React.useMemo(() => {
return (scripts || [])
.filter(
(script) =>
(script.type === undefined || script.type === ScriptType.Content) &&
!script.bulk &&
!script.hidden
)
.map((script) => (
<MenuItem
key={script.title}
title={
<div className="flex items-center">
<CommandLineIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} />{' '}
<span>{script.title}</span>
</div>
}
onClick={(value, e) => runCustomScript(e, script)}
/>
));
}, [scripts]);
return (
<>
<div
className={`${listView ? '' : 'group/card absolute top-6 right-0'
className={`${listView ? '' : 'group/card absolute top-6 right-2'
} flex flex-col space-y-4`}
>
<div
className={`flex items-center border border-transparent rounded-full ${listView ? '' : 'p-2 -mt-4'
className={`flex items-center border border-transparent rounded-full ${listView ? '' : 'p-1 -mt-3'
} group-hover/card:bg-[var(--vscode-sideBar-background)] group-hover/card:border-[var(--frontmatter-border)]`}
>
<Menu as="div" className={`relative flex text-left`}>
{!listView && (
<div className="hidden group-hover/card:flex">
<QuickAction title={l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)} onClick={onView}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<div className={`relative flex text-left`}>
<DropdownMenu>
<DropdownMenuTrigger
className='text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)] data-[state=open]:text-[var(--vscode-tab-activeForeground)] focus:outline-none'>
<span className="sr-only">{l10n.t(LocalizationKey.dashboardContentsContentActionsActionMenuButtonTitle)}</span>
<EllipsisHorizontalIcon className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={(e) => isPinned ? unpinItem(e) : pinItem(e)}>
<PinIcon className={`mr-2 h-4 w-4 ${isPinned ? "" : "-rotate-90"}`} aria-hidden={true} />
<span>{isPinned ? l10n.t(LocalizationKey.commonUnpin) : l10n.t(LocalizationKey.commonPin)}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onView}>
<EyeIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onRename}>
<RenameIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonRename)}</span>
</DropdownMenuItem>
{
settings?.websiteUrl && (
<QuickAction title={l10n.t(LocalizationKey.commonOpenOnWebsite)} onClick={openOnWebsite}>
<GlobeEuropeAfricaIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<DropdownMenuItem onClick={onOpenWebsite}>
<GlobeEuropeAfricaIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonOpenOnWebsite)}</span>
</DropdownMenuItem>
)
}
<QuickAction title={l10n.t(LocalizationKey.commonDelete)} onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
)}
<div ref={setReferenceElement} className={`flex`}>
<ActionMenuButton title={l10n.t(LocalizationKey.dashboardContentsContentActionsActionMenuButtonTitle)} />
</div>
<div
className="menu_items__wrapper z-20"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<MenuItems
updatePopper={forceUpdate || undefined}
widthClass="w-44"
marginTopClass={listView ? '' : ''}
>
<MenuItem
title={
<div className="flex items-center">
<PinIcon className={`mr-2 h-5 w-5 flex-shrink-0 ${isPinned ? "" : "-rotate-90"}`} aria-hidden={true} />{' '}
<span>{isPinned ? l10n.t(LocalizationKey.commonUnpin) : l10n.t(LocalizationKey.commonPin)}</span>
</div>
}
onClick={(_, e) => isPinned ? unpinItem(e) : pinItem(e)}
/>
<MenuItem
title={
<div className="flex items-center">
<EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} />{' '}
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}</span>
</div>
}
onClick={(_, e) => onView(e)}
/>
{
settings?.websiteUrl && (
<MenuItem
title={
<div className="flex items-center">
<GlobeEuropeAfricaIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} />{' '}
<span>{l10n.t(LocalizationKey.commonOpenOnWebsite)}</span>
</div>
}
onClick={(_, e) => openOnWebsite(e)}
/>
locale && (
<DropdownMenuItem onClick={() => runCommand(COMMAND_NAME.i18n.create)}>
<LanguageIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsCreate)}</span>
</DropdownMenuItem>
)
}
{customScriptActions}
<TranslationMenu
isDefaultLocale={isDefaultLocale}
locale={locale}
translations={translations} />
<MenuItem
title={
<div className="flex items-center">
<TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} />{' '}
<span>{l10n.t(LocalizationKey.commonDelete)}</span>
</div>
}
onClick={(_, e) => onDelete(e)}
/>
</MenuItems>
</div>
</Menu>
<CustomActions
filePath={path}
contentType={contentType}
scripts={scripts} />
<DropdownMenuItem onClick={onDelete} className={`focus:bg-[var(--vscode-statusBarItem-errorBackground)] focus:text-[var(--vscode-statusBarItem-errorForeground)]`}>
<TrashIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonDelete)}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
{showDeletionAlert && (
<Alert
title={l10n.t(LocalizationKey.dashboardContentsContentActionsAlertTitle, title)}
description={l10n.t(LocalizationKey.dashboardContentsContentActionsAlertDescription, title)}
okBtnText={l10n.t(LocalizationKey.commonDelete)}
cancelBtnText={l10n.t(LocalizationKey.commonCancel)}
dismiss={() => setShowDeletionAlert(false)}
trigger={onDeleteConfirm}
/>
)}
</>
);
};

View File

@@ -1,16 +1,21 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import * as l10n from '@vscode/l10n';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Page } from '../../models';
import { LoadingAtom, SettingsSelector } from '../../state';
import { LoadingAtom, SelectedItemActionAtom, SettingsSelector } from '../../state';
import { Overview } from './Overview';
import { Spinner } from '../Common/Spinner';
import { SponsorMsg } from '../Layout/SponsorMsg';
import usePages from '../../hooks/usePages';
import { useEffect } from 'react';
import { useCallback, useEffect, useState } from 'react';
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';
import { Alert } from '../Modals/Alert';
import { LocalizationKey } from '../../../localization';
import { deletePage } from '../../utils';
export interface IContentsProps {
pages: Page[];
@@ -22,9 +27,38 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({
const loading = useRecoilValue(LoadingAtom);
const settings = useRecoilValue(SettingsSelector);
const { pageItems } = usePages(pages);
const [showDeletionAlert, setShowDeletionAlert] = React.useState(false);
const [page, setPage] = useState<Page | undefined>(undefined);
const [selectedItemAction, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
const pageFolders = [...new Set(pageItems.map((page) => page.fmFolder))];
const onDismiss = useCallback(() => {
setShowDeletionAlert(false);
setSelectedItemAction(undefined);
}, []);
const onDeleteConfirm = useCallback(() => {
if (page) {
deletePage(page.fmFilePath);
}
setShowDeletionAlert(false);
setSelectedItemAction(undefined);
}, [page]);
useEffect(() => {
if (selectedItemAction && selectedItemAction.path && selectedItemAction.action === 'delete') {
const page = pageItems.find((p) => p.fmFilePath === selectedItemAction.path);
if (page) {
setPage(page);
setShowDeletionAlert(true);
}
setSelectedItemAction(undefined);
}
}, [pageItems, selectedItemAction]);
useEffect(() => {
Messenger.send(DashboardMessage.sendTelemetry, {
event: TelemetryEvent.webviewContentsView
@@ -32,18 +66,31 @@ 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" />
{showDeletionAlert && page && (
<Alert
title={l10n.t(LocalizationKey.dashboardContentsContentActionsAlertTitle, page.title)}
description={l10n.t(LocalizationKey.dashboardContentsContentActionsAlertDescription, page.title)}
okBtnText={l10n.t(LocalizationKey.commonDelete)}
cancelBtnText={l10n.t(LocalizationKey.commonCancel)}
dismiss={onDismiss}
trigger={onDeleteConfirm}
/>
)}
</PageLayout>
</FilesProvider>
);
};

View File

@@ -0,0 +1,89 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { CommandLineIcon } from '@heroicons/react/24/outline';
import { CommandLineIcon as CommandLineIconSolid } from '@heroicons/react/24/solid';
import { runCustomScript } from '../../utils';
import { CustomScript, ScriptType } from '../../../models';
import { LocalizationKey } from '../../../localization';
export interface ICustomActionsProps {
filePath: string;
contentType: string;
scripts: CustomScript[] | undefined;
showTrigger?: boolean;
}
export const CustomActions: React.FunctionComponent<ICustomActionsProps> = ({
filePath,
contentType,
scripts,
showTrigger = false,
}: React.PropsWithChildren<ICustomActionsProps>) => {
const onRunCustomScript = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>, script: CustomScript) => {
e.stopPropagation();
runCustomScript(script, filePath);
},
[filePath]
);
const customScripts = React.useMemo(() => {
return (scripts || []).filter((script: CustomScript) => {
if (script.contentTypes && script.contentTypes.length > 0) {
return script.contentTypes.includes(contentType);
}
return true;
});
}, [scripts, contentType]);
const customActions = React.useMemo(() => {
if (!customScripts || customScripts.length === 0) {
return null;
}
return (
(customScripts || [])
.filter(
(script) =>
(script.type === undefined || script.type === ScriptType.Content) &&
!script.bulk &&
!script.hidden
)
.map((script) => (
<DropdownMenuItem
key={script.id || script.title}
title={script.title}
onClick={(e) => onRunCustomScript(e, script)}>
<CommandLineIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{script.title}</span>
</DropdownMenuItem>
))
);
}, [customScripts, onRunCustomScript]);
if (!customActions || customActions.length === 0) {
return null;
}
if (showTrigger) {
return (
<DropdownMenu>
<DropdownMenuTrigger
title={l10n.t(LocalizationKey.commonOpenCustomActions)}
className='px-2 text-[var(--frontmatter-secondary-text)] hover:text-[var(--frontmatter-button-hoverBackground)] focus-visible:outline-none'>
<span className="sr-only">{l10n.t(LocalizationKey.commonOpenCustomActions)}</span>
<CommandLineIconSolid className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{customActions}
</DropdownMenuContent>
</DropdownMenu>
);
}
return <>{customActions}</>;
};

View File

@@ -0,0 +1,66 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { QuickAction } from '../Menu';
import { EyeIcon, GlobeEuropeAfricaIcon, TrashIcon } from '@heroicons/react/24/solid';
import { LocalizationKey } from '../../../localization';
import { openFile, openOnWebsite } from '../../utils';
import { useRecoilState } from 'recoil';
import { SelectedItemActionAtom } from '../../state';
import { CustomScript } from '../../../models';
import { CustomActions } from './CustomActions';
export interface IFooterActionsProps {
filePath: string;
contentType: string;
websiteUrl?: string;
scripts?: CustomScript[];
}
export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
filePath,
contentType,
websiteUrl,
scripts
}: React.PropsWithChildren<IFooterActionsProps>) => {
const [, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
return (
<div className={`py-2 w-full flex items-center justify-evenly border-t border-t-[var(--frontmatter-border)] bg-[var(--frontmatter-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)] rounded`}>
{/* <ItemSelection filePath={filePath} show /> */}
<QuickAction
title={l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}
className={`text-[var(--frontmatter-secondary-text)]`}
onClick={() => openFile(filePath)}>
<span className={`sr-only`}>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}</span>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
{
websiteUrl && (
<QuickAction
title={l10n.t(LocalizationKey.commonOpenOnWebsite)}
className={`text-[var(--frontmatter-secondary-text)]`}
onClick={() => openOnWebsite(websiteUrl, filePath)}>
<span className={`sr-only`}>{l10n.t(LocalizationKey.commonOpenOnWebsite)}</span>
<GlobeEuropeAfricaIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)
}
<CustomActions
filePath={filePath}
contentType={contentType}
scripts={scripts}
showTrigger />
<QuickAction
title={l10n.t(LocalizationKey.commonDelete)}
className={`text-[var(--frontmatter-secondary-text)] hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
onClick={() => setSelectedItemAction({ path: filePath, action: 'delete' })}>
<span className={`sr-only`}>{l10n.t(LocalizationKey.commonDelete)}</span>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
);
};

View File

@@ -0,0 +1,26 @@
import * as React from 'react';
import { Page } from '../../models';
import { ChevronDownIcon, LanguageIcon } from '@heroicons/react/24/outline';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { MenuItem } from '../Menu';
import { DashboardMessage } from '../../DashboardMessage';
import { messageHandler } from '@estruyf/vscode/dist/client';
export interface II18nLabelProps {
page: Page;
}
export const I18nLabel: React.FunctionComponent<II18nLabelProps> = ({
page
}: React.PropsWithChildren<II18nLabelProps>) => {
if (!page.fmLocale) {
return null;
}
return (
<div className="mb-2 flex items-center">
<LanguageIcon className="mr-1 h-4 w-4 inline-block" />
<span className="text-xs">{page.fmLocale.title || page.fmLocale.locale}</span>
</div>
);
};

View File

@@ -1,10 +1,8 @@
import { useRecoilValue } from 'recoil';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { DashboardMessage } from '../../DashboardMessage';
import { Page } from '../../models/Page';
import { SettingsSelector, ViewSelector } from '../../state';
import { DateField } from '../Common/DateField';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardViewType } from '../../models';
import { ContentActions } from './ContentActions';
import { useMemo } from 'react';
@@ -13,9 +11,14 @@ import * as React from 'react';
import useExtensibility from '../../hooks/useExtensibility';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { useNavigate } from 'react-router-dom';
import { routePaths } from '../..';
import useCard from '../../hooks/useCard';
import { I18nLabel } from './I18nLabel';
import { ItemSelection } from '../Common/ItemSelection';
import { openFile } from '../../utils';
import { FooterActions } from './FooterActions';
import useSelectedItems from '../../hooks/useSelectedItems';
import { cn } from '../../../utils/cn';
import { Tags } from './Tags';
export interface IItemProps extends Page { }
@@ -24,12 +27,12 @@ const PREVIEW_IMAGE_FIELD = 'fmPreviewImage';
export const Item: React.FunctionComponent<IItemProps> = ({
...pageData
}: React.PropsWithChildren<IItemProps>) => {
const { selectedFiles } = useSelectedItems();
const view = useRecoilValue(ViewSelector);
const settings = useRecoilValue(SettingsSelector);
const draftField = useMemo(() => settings?.draftField, [settings]);
const cardFields = useMemo(() => settings?.dashboardState?.contents?.cardFields, [settings?.dashboardState?.contents?.cardFields]);
const { escapedTitle, escapedDescription } = useCard(pageData, settings?.dashboardState?.contents?.cardFields);
const navigate = useNavigate();
const { titleHtml, descriptionHtml, dateHtml, statusHtml, tagsHtml, imageHtml, footerHtml } = useExtensibility({
fmFilePath: pageData.fmFilePath,
date: pageData.date,
@@ -39,9 +42,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({
pageData
});
const openFile = () => {
Messenger.send(DashboardMessage.openFile, pageData.fmFilePath);
};
const isSelected = useMemo(() => selectedFiles.includes(pageData.fmFilePath), [selectedFiles, pageData.fmFilePath]);
const onOpenFile = React.useCallback(() => {
openFile(pageData.fmFilePath);
}, [pageData.fmFilePath]);
const tags: string[] | undefined = useMemo(() => {
if (!settings?.dashboardState?.contents?.tags) {
@@ -69,6 +74,34 @@ export const Item: React.FunctionComponent<IItemProps> = ({
return [];
}, [settings, pageData]);
const statusPlaceholder = useMemo(() => {
if (!statusHtml && !cardFields?.state) {
return null;
}
return (
statusHtml ? (
<div dangerouslySetInnerHTML={{ __html: statusHtml }} />
) : (
cardFields?.state && draftField && draftField.name && typeof pageData[draftField.name] !== "undefined" ? <Status draft={pageData[draftField.name]} published={pageData.fmPublished} /> : null
)
)
}, [statusHtml, cardFields?.state, draftField, pageData]);
const datePlaceholder = useMemo(() => {
if (!dateHtml && !cardFields?.date) {
return null;
}
return (
dateHtml ? (
<div className='mr-6' dangerouslySetInnerHTML={{ __html: dateHtml }} />
) : (
cardFields?.date && pageData.date ? <DateField className={`mr-6`} value={pageData.date} format={pageData.fmDateFormat} /> : null
)
)
}, [dateHtml, cardFields?.date, pageData]);
const hasDraftOrDate = useMemo(() => {
return cardFields && (cardFields.state || cardFields.date);
}, [cardFields]);
@@ -77,12 +110,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({
return (
<li className="relative">
<div
className={`group flex flex-col items-start content-start h-full w-full text-left shadow-md dark:shadow-none hover:shadow-xl border rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-sideBarTitle-foreground)] border-[var(--frontmatter-border)]`}
className={cn(`group flex flex-col items-start content-start h-full w-full text-left shadow-md dark:shadow-none hover:shadow-xl border rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-sideBarTitle-foreground)] border-[var(--frontmatter-border)]`, isSelected && `border-[var(--frontmatter-border-active)]`)}
>
<button
onClick={openFile}
className={`relative h-36 w-full overflow-hidden border-b cursor-pointer border-[var(--frontmatter-border)]
}`}
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={onOpenFile}
className={`relative rounded h-36 w-full overflow-hidden border-b cursor-pointer border-[var(--frontmatter-border)]`}
>
{
imageHtml ?
@@ -104,80 +137,68 @@ export const Item: React.FunctionComponent<IItemProps> = ({
}
</button>
<div className="relative p-4 w-full grow">
<div className={`flex justify-between items-center ${hasDraftOrDate ? `mb-2` : ``}`}>
{
statusHtml ? (
<div dangerouslySetInnerHTML={{ __html: statusHtml }} />
) : (
cardFields?.state && draftField && draftField.name && <Status draft={pageData[draftField.name]} published={pageData.fmPublished} />
)
}
<ItemSelection filePath={pageData.fmFilePath} />
{
dateHtml ? (
<div className='mr-4' dangerouslySetInnerHTML={{ __html: dateHtml }} />
) : (
cardFields?.date && <DateField className={`mr-4`} value={pageData.date} format={pageData.fmDateFormat} />
)
}
</div>
<div className="relative p-4 w-full grow">
{
(statusPlaceholder || datePlaceholder) && (
<div className={`space-y-2 ${hasDraftOrDate ? `mb-2` : ``}`}>
<div>{statusPlaceholder}</div>
<div>{datePlaceholder}</div>
</div>
)
}
<ContentActions
title={pageData.title}
path={pageData.fmFilePath}
relPath={pageData.fmRelFileWsPath}
contentType={pageData.fmContentType}
locale={pageData.fmLocale}
isDefaultLocale={pageData.fmDefaultLocale}
translations={pageData.fmTranslations}
scripts={settings?.scripts}
onOpen={openFile}
onOpen={onOpenFile}
/>
<button onClick={openFile} className={`text-left block`}>
<I18nLabel page={pageData} />
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={onOpenFile}
className={`text-left block`}>
{
titleHtml ? (
<div dangerouslySetInnerHTML={{ __html: titleHtml }} />
) : (
<h2 className="mb-2 font-bold">
{escapedTitle}
<h2 className="font-bold">
<span>{escapedTitle}</span>
</h2>
)
}
</button>
<button onClick={openFile} className={`text-left block`}>
{
descriptionHtml ? (
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
) : (
<p className={`text-xs text-[vara(--vscode-titleBar-activeForeground)]`}>{escapedDescription}</p>
)
}
</button>
{
(escapedDescription || descriptionHtml) && (
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={onOpenFile}
className={`mt-2 text-left block`}>
{
descriptionHtml ? (
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
) : (
<p className={`text-xs text-[var(--frontmatter-secondary-text)]`}>{escapedDescription}</p>
)
}
</button>
)
}
{
tagsHtml ? (
<div className="mt-2" dangerouslySetInnerHTML={{ __html: tagsHtml }} />
) : (
tags && tags.length > 0 && (
<div className="mt-2">
{tags.map(
(tag, index) => tag && (
<button
key={index}
className={`inline-block mr-1 mt-1 text-xs text-[var(--vscode-textPreformat-foreground)] hover:brightness-75 hover:underline hover:underline-offset-1`}
title={l10n.t(LocalizationKey.commonFilterValue, tag)}
onClick={() => {
const tagField = settings?.dashboardState.contents.tags;
if (tagField) {
navigate(`${routePaths.contents}?taxonomy=${tagField}&value=${tag}`);
}
}}
>
#{tag}
</button>
)
)}
</div>
)
<Tags values={tags} tagField={settings?.dashboardState?.contents?.tags} />
)
}
</div>
@@ -187,6 +208,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({
<div className="placeholder__card__footer p-4 w-full" dangerouslySetInnerHTML={{ __html: footerHtml }} />
)
}
<FooterActions
filePath={pageData.fmFilePath}
contentType={pageData.fmContentType}
websiteUrl={settings?.websiteUrl}
scripts={settings?.scripts} />
</div>
</li>
);
@@ -197,16 +224,20 @@ 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">
<button title={`Open: ${escapedTitle}`} onClick={openFile}>
<ItemSelection filePath={pageData.fmFilePath} show />
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={onOpenFile}>
{escapedTitle}
</button>
<ContentActions
title={escapedTitle || ""}
path={pageData.fmFilePath}
relPath={pageData.fmRelFileWsPath}
contentType={pageData.fmContentType}
scripts={settings?.scripts}
onOpen={openFile}
onOpen={onOpenFile}
listView
/>
</div>

View File

@@ -2,27 +2,32 @@ import * as React from 'react';
import { Page } from '../../models';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { ContentActions } from './ContentActions';
import { DashboardMessage } from '../../DashboardMessage';
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';
import { openFile } from '../../utils';
import useSelectedItems from '../../hooks/useSelectedItems';
import { cn } from '../../../utils/cn';
export interface IPinnedItemProps extends Page { }
export const PinnedItem: React.FunctionComponent<IPinnedItemProps> = ({
...pageData
}: React.PropsWithChildren<IPinnedItemProps>) => {
const { selectedFiles } = useSelectedItems();
const settings = useRecoilValue(SettingsSelector);
const { escapedTitle } = useCard(pageData, settings?.dashboardState?.contents?.cardFields);
const openFile = React.useCallback(() => {
messageHandler.send(DashboardMessage.openFile, pageData.fmFilePath);
const isSelected = React.useMemo(() => selectedFiles.includes(pageData.fmFilePath), [selectedFiles, pageData.fmFilePath]);
const onOpenFile = React.useCallback(() => {
openFile(pageData.fmFilePath);
}, [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)]'>
<button onClick={openFile} className='relative h-full w-1/3'>
<li className={cn(`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`, isSelected && `border-[var(--frontmatter-border-active)]`)}>
<button onClick={onOpenFile} className='relative h-full w-1/3'>
{
pageData["fmPreviewImage"] ? (
<img
@@ -41,13 +46,15 @@ export const PinnedItem: React.FunctionComponent<IPinnedItemProps> = ({
}
</button>
<button onClick={openFile} className='relative w-2/3 p-4 pr-6 text-left flex items-start'>
<ItemSelection filePath={pageData.fmFilePath} />
<button onClick={onOpenFile} className='relative w-2/3 p-4 pr-6 text-left flex items-start'>
<p className='font-bold'>{escapedTitle}</p>
<ContentActions
title={pageData.title}
path={pageData.fmFilePath}
relPath={pageData.fmRelFileWsPath}
contentType={pageData.fmContentType}
scripts={settings?.scripts}
onOpen={openFile}
/>

View File

@@ -34,7 +34,7 @@ export const Status: React.FunctionComponent<IStatusProps> = ({
if (draftValue) {
return (
<span
className={`inline-block px-1 py-1 leading-none rounded-sm font-semibold uppercase tracking-wide text-[0.7rem] text-[var(--vscode-badge-foreground)] bg-[var(--vscode-badge-background)]`}
className={`inline-block px-[3px] py-[2px] rounded font-semibold uppercase tracking-wide text-[0.7rem] text-[var(--vscode-badge-foreground)] bg-[var(--vscode-badge-background)]`}
>
{draftValue}
</span>
@@ -51,7 +51,7 @@ export const Status: React.FunctionComponent<IStatusProps> = ({
return (
<span
className={`draft__status
inline-block px-1 py-1 leading-none rounded-sm font-semibold uppercase tracking-wide text-[0.7rem]
inline-block px-[3px] py-[2px] rounded font-semibold uppercase tracking-wide text-[0.7rem]
${draftValue ?
'bg-[var(--vscode-statusBarItem-errorBackground)] text-[var(--vscode-statusBarItem-errorForeground)]' :
isFuture ?

View File

@@ -0,0 +1,35 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { routePaths } from '../..';
import { LocalizationKey } from '../../../localization';
import { useNavigate } from 'react-router-dom';
export interface ITagProps {
value?: string;
tagField?: string | null | undefined;
}
export const Tag: React.FunctionComponent<ITagProps> = ({
value,
tagField
}: React.PropsWithChildren<ITagProps>) => {
const navigate = useNavigate();
if (!value) {
return null;
}
return (
<button
className={`inline-block mr-1 mt-1 text-xs text-[var(--vscode-button-secondaryForeground)] bg-[var(--vscode-button-secondaryBackground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)] border border-[var(--frontmatter-border)] rounded px-1 py-0.5`}
title={l10n.t(LocalizationKey.commonFilterValue, value)}
onClick={() => {
if (tagField) {
navigate(`${routePaths.contents}?taxonomy=${tagField}&value=${value}`);
}
}}
>
#{value}
</button>
);
};

View File

@@ -0,0 +1,29 @@
import * as React from 'react';
import { Tag } from './Tag';
export interface ITagsProps {
values?: string[];
tagField?: string | null | undefined;
}
export const Tags: React.FunctionComponent<ITagsProps> = ({
values,
tagField
}: React.PropsWithChildren<ITagsProps>) => {
if (!values || values.length === 0) {
return null;
}
return (
<div className="mt-2">
{values.map(
(tag, index) => tag && (
<Tag
key={index}
value={tag}
tagField={tagField} />
)
)}
</div>
);
};

View File

@@ -0,0 +1,80 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { DropdownMenuItem, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '../../../components/shadcn/Dropdown';
import { LanguageIcon } from '@heroicons/react/24/outline';
import { openFile } from '../../utils/MessageHandlers';
import { I18nConfig } from '../../../models';
import { LocalizationKey } from '../../../localization';
export interface ITranslationMenuProps {
isDefaultLocale?: boolean;
locale?: I18nConfig;
translations?: {
[locale: string]: {
locale: I18nConfig;
path: string;
};
};
}
export const TranslationMenu: React.FunctionComponent<ITranslationMenuProps> = ({
isDefaultLocale,
locale,
translations,
}: React.PropsWithChildren<ITranslationMenuProps>) => {
const otherLocales = React.useMemo(() => {
if (!translations) {
return [];
}
return Object.entries(translations).filter(([key]) => key !== locale?.locale);
}, [translations]);
const crntLocale = React.useMemo(() => {
if (!locale?.locale || !translations || !translations[locale.locale]) {
return null;
}
return translations[locale.locale];
}, [translations, locale]);
if (!locale || !translations || Object.keys(translations).length === 0) {
return null;
}
if (otherLocales.length === 0 || !crntLocale) {
return null;
}
return (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<LanguageIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsMenu)}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => openFile(crntLocale.path)}>
<span>{crntLocale.locale.title || crntLocale.locale.locale}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
{
otherLocales.map(([key, value]) => (
<DropdownMenuItem
key={key}
onClick={() => openFile(value.path)}
>
<span>{value.locale.title || value.locale.locale}</span>
</DropdownMenuItem>
))
}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
);
};

View File

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

View File

@@ -0,0 +1,66 @@
import * as React from 'react';
import { DropdownMenu, DropdownMenuContent, DropdownMenuSeparator } from '../../../components/shadcn/Dropdown';
import { LanguageIcon } from '@heroicons/react/24/outline';
import { MenuButton, MenuItem } from '../Menu';
import { useRecoilState, useRecoilValue } from 'recoil';
import { DEFAULT_LOCALE_STATE, LocaleAtom, LocalesAtom } from '../../state';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
export interface ILanguageFilterProps { }
export const LanguageFilter: React.FunctionComponent<ILanguageFilterProps> = ({ }: React.PropsWithChildren<ILanguageFilterProps>) => {
const locales = useRecoilValue(LocalesAtom);
const [crntLocale, setCrntLocale] = useRecoilState(LocaleAtom);
const crntLocaleName = React.useMemo(() => {
if (!crntLocale || !locales || locales.length === 0) {
return null;
}
const locale = locales.find(locale => locale.locale === crntLocale);
return locale?.title || locale?.locale;
}, [crntLocale, locales]);
if (!locales || locales.length <= 1) {
return null;
}
return (
<DropdownMenu>
<MenuButton
label={
<>
<LanguageIcon className={`inline-block w-4 h-4 mr-2`} />
<span>{l10n.t(LocalizationKey.dashboardFiltersLanguageFilterLabel)}</span>
</>
}
title={crntLocaleName || l10n.t(LocalizationKey.dashboardFiltersLanguageFilterAll)}
/>
<DropdownMenuContent align='start'>
<MenuItem
title={l10n.t(LocalizationKey.dashboardFiltersLanguageFilterAll)}
value={null}
isCurrent={crntLocale === DEFAULT_LOCALE_STATE}
onClick={() => setCrntLocale(DEFAULT_LOCALE_STATE)}
/>
<DropdownMenuSeparator />
{
locales.map((locale) => (
<MenuItem
key={locale.locale}
title={locale.title || locale.locale}
value={locale.locale}
isCurrent={locale.locale === crntLocale}
onClick={(value) => setCrntLocale(value)}
/>
))
}
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -0,0 +1,272 @@
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';
import { RenameIcon } from '../../../components/icons/RenameIcon';
import { openFile } from '../../utils';
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) {
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={() => openFile(crntLocale.path)}>
<span>{crntLocale.locale.title || crntLocale.locale.locale}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
{
otherLocales.map(([key, value]) => (
<DropdownMenuItem
key={key}
onClick={() => 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.Contents && (
<ActionsBarItem
disabled={selectedFiles.length === 0 || selectedFiles.length > 1}
onClick={() => {
messageHandler.send(DashboardMessage.rename, selectedFiles[0]);
setSelectedFiles([]);
}}
>
<RenameIcon className="w-4 h-4 mr-2" aria-hidden="true" />
<span>{l10n.t(LocalizationKey.commonRename)}</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, selectedFiles.length)}</span>
</button>
)
}
</div>
{showAlert && (
<Alert
title={`${l10n.t(LocalizationKey.dashboardHeaderActionsBarAlertDeleteTitle)}`}
description={l10n.t(LocalizationKey.dashboardHeaderActionsBarAlertDeleteDescription)}
okBtnText={l10n.t(LocalizationKey.commonDelete)}
cancelBtnText={l10n.t(LocalizationKey.commonCancel)}
dismiss={() => setShowAlert(false)}
trigger={onDeleteConfirm}
/>
)}
</>
);
};

View File

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

View File

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

View File

@@ -12,7 +12,9 @@ import {
CategoryAtom,
DEFAULT_TAG_STATE,
DEFAULT_CATEGORY_STATE,
FiltersAtom
FiltersAtom,
LocaleAtom,
DEFAULT_LOCALE_STATE
} from '../../state';
import { DefaultValue } from 'recoil';
import { useEffect, useMemo } from 'react';
@@ -34,12 +36,14 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (
const folder = useRecoilValue(FolderSelector);
const tag = useRecoilValue(TagSelector);
const category = useRecoilValue(CategorySelector);
const locale = useRecoilValue(LocaleAtom);
const filters = useRecoilValue(FiltersAtom);
const resetSorting = useResetRecoilState(SortingAtom);
const resetFolder = useResetRecoilState(FolderAtom);
const resetTag = useResetRecoilState(TagAtom);
const resetCategory = useResetRecoilState(CategoryAtom);
const resetLocale = useResetRecoilState(LocaleAtom);
const resetFilters = useResetRecoilState(FiltersAtom);
const reset = () => {
@@ -48,6 +52,7 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (
resetFolder();
resetTag();
resetCategory();
resetLocale();
resetFilters();
};
@@ -61,19 +66,20 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (
folder !== DEFAULT_FOLDER_STATE ||
tag !== DEFAULT_TAG_STATE ||
category !== DEFAULT_CATEGORY_STATE ||
locale !== DEFAULT_LOCALE_STATE ||
hasCustomFilters
) {
setShow(true);
} else {
setShow(false);
}
}, [folder, tag, category, hasCustomFilters]);
}, [folder, tag, category, locale, hasCustomFilters]);
if (!show) return null;
return (
<button
className={`flex items-center hover:text-[var(--vscode-textLink-activeForeground)]`}
className={`flex items-center hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
onClick={reset}
title={l10n.t(LocalizationKey.dashboardHeaderClearFiltersTitle)}
>

View File

@@ -1,9 +1,9 @@
import { Menu } from '@headlessui/react';
import { FunnelIcon } from '@heroicons/react/24/solid';
import * as React from 'react';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
import { MenuButton, MenuItem } from '../Menu';
import * as l10n from '@vscode/l10n';
import { FunnelIcon } from '@heroicons/react/24/solid';
import { LocalizationKey } from '../../../localization';
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
export interface IFilterProps {
label: string;
@@ -25,37 +25,35 @@ export const Filter: React.FunctionComponent<IFilterProps> = ({
}
return (
<div className="flex items-center">
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton
label={
<>
<FunnelIcon className={`inline-block w-5 h-5 mr-1`} />
<span>{label}</span>
</>
}
title={activeItem || DEFAULT_VALUE}
<DropdownMenu>
<MenuButton
label={
<>
<FunnelIcon className={`inline-block w-4 h-4 mr-2`} />
<span>{label}</span>
</>
}
title={activeItem || DEFAULT_VALUE}
/>
<DropdownMenuContent>
<MenuItem
title={DEFAULT_VALUE}
value={null}
isCurrent={!activeItem}
onClick={() => onClick(null)}
/>
<MenuItems disablePopper>
{items.map((option) => (
<MenuItem
title={DEFAULT_VALUE}
value={null}
isCurrent={!!activeItem}
onClick={() => onClick(null)}
key={option}
title={option}
value={option}
isCurrent={option === activeItem}
onClick={() => onClick(option)}
/>
{items.map((option) => (
<MenuItem
key={option}
title={option}
value={option}
isCurrent={option === activeItem}
onClick={() => onClick(option)}
/>
))}
</MenuItems>
</Menu>
</div>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -6,6 +6,7 @@ import { CategoryAtom, SettingsSelector, TagAtom, FiltersAtom, FilterValuesAtom
import { useEffect, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { firstToUpper } from '../../../helpers/StringHelpers';
import { LanguageFilter } from '../Filters/LanguageFilter';
export interface IFiltersProps { }
@@ -17,8 +18,7 @@ export const Filters: React.FunctionComponent<IFiltersProps> = (_: React.PropsWi
const settings = useRecoilValue(SettingsSelector);
const location = useLocation();
const otherFilters = useMemo(() => settings?.filters?.filter((filter) => filter !== "pageFolders" && filter !== "tags" && filter !== "categories"), [settings?.filters]);
const otherFilters = useMemo(() => settings?.filters?.filter((filter) => filter !== "contentFolders" && filter !== "tags" && filter !== "categories"), [settings?.filters]);
const otherFilterValues = useMemo(() => {
return otherFilters?.map((filter) => {
@@ -74,8 +74,10 @@ export const Filters: React.FunctionComponent<IFiltersProps> = (_: React.PropsWi
return (
<>
<LanguageFilter />
{
settings?.filters?.includes("pageFolders") && (
settings?.filters?.includes("contentFolders") && (
<FoldersFilter />
)
}

View File

@@ -1,10 +1,10 @@
import { Menu } from '@headlessui/react';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { FolderAtom, SettingsSelector } from '../../state';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
import { MenuButton, MenuItem } from '../Menu';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
export interface IFoldersFilterProps { }
@@ -14,36 +14,38 @@ 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;
}
return (
<div className="flex items-center">
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton label={l10n.t(LocalizationKey.dashboardHeaderFoldersMenuButtonShowing)} title={crntFolder || DEFAULT_TYPE} />
<DropdownMenu>
<MenuButton label={l10n.t(LocalizationKey.dashboardHeaderFoldersMenuButtonShowing)} title={crntFolder || DEFAULT_TYPE} />
<MenuItems disablePopper>
<DropdownMenuContent>
<MenuItem
title={DEFAULT_TYPE}
value={null}
isCurrent={!crntFolder}
onClick={(value) => setCrntFolder(value)}
/>
{contentFolders.map((option) => (
<MenuItem
title={DEFAULT_TYPE}
value={null}
isCurrent={!crntFolder}
key={option.title}
title={option.title}
value={option.title}
isCurrent={option.title === crntFolder}
onClick={(value) => setCrntFolder(value)}
/>
{contentFolders.map((option) => (
<MenuItem
key={option.title}
title={option.title}
value={option.title}
isCurrent={option.title === crntFolder}
onClick={(value) => setCrntFolder(value)}
/>
))}
</MenuItems>
</Menu>
</div>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -1,11 +1,11 @@
import { Menu } from '@headlessui/react';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { GroupOption } from '../../constants/GroupOption';
import { AllPagesAtom, GroupingAtom } from '../../state';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
import { MenuButton, MenuItem } from '../Menu';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
export interface IGroupingProps { }
@@ -42,22 +42,20 @@ export const Grouping: React.FunctionComponent<
}
return (
<div className="flex items-center">
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton label={l10n.t(LocalizationKey.dashboardHeaderGroupingMenuButtonLabel)} title={crntGroup?.name || ''} />
<DropdownMenu>
<MenuButton label={l10n.t(LocalizationKey.dashboardHeaderGroupingMenuButtonLabel)} title={crntGroup?.name || ''} />
<MenuItems disablePopper>
{GROUP_OPTIONS.map((option) => (
<MenuItem
key={option.id}
title={option.name}
value={option.id}
isCurrent={option.id === crntGroup?.id}
onClick={(value) => setGroup(value)}
/>
))}
</MenuItems>
</Menu>
</div>
<DropdownMenuContent>
{GROUP_OPTIONS.map((option) => (
<MenuItem
key={option.id}
title={option.name}
value={option.id}
isCurrent={option.id === crntGroup?.id}
onClick={(value) => setGroup(value)}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -6,7 +6,7 @@ import { DashboardMessage } from '../../DashboardMessage';
import { Grouping } from '.';
import { ViewSwitch } from './ViewSwitch';
import { useRecoilValue, useResetRecoilState } from 'recoil';
import { GroupingSelector, SortingAtom } from '../../state';
import { GroupingSelector, MultiSelectedItemsAtom, SortingAtom } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { ClearFilters } from './ClearFilters';
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
@@ -18,8 +18,7 @@ import { ArrowTopRightOnSquareIcon, BoltIcon, PlusIcon } from '@heroicons/react/
import { HeartIcon } from '@heroicons/react/24/solid';
import { useLocation, useNavigate } from 'react-router-dom';
import { routePaths } from '../..';
import { useEffect, useMemo } from 'react';
import { SyncButton } from './SyncButton';
import { useMemo } from 'react';
import { Pagination } from './Pagination';
import { GroupOption } from '../../constants/GroupOption';
import usePagination from '../../hooks/usePagination';
@@ -32,6 +31,8 @@ import { SettingsLink } from '../SettingsView/SettingsLink';
import { Link } from '../Common/Link';
import { SPONSOR_LINK } from '../../../constants';
import { Filters } from './Filters';
import { ActionsBar } from './ActionsBar';
import { RefreshDashboardData } from './RefreshDashboardData';
export interface IHeaderProps {
header?: React.ReactNode;
@@ -51,6 +52,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 +72,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
const updateView = (view: NavigationType) => {
navigate(routePaths[view]);
resetSorting();
resetSelectedItems();
};
const runBulkScript = (script: CustomScript) => {
@@ -122,10 +125,10 @@ 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={`px-4 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'>
<div className='flex items-center space-x-2'>
<ProjectSwitcher />
{
@@ -160,19 +163,19 @@ 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-2 flex-1`}>
<ChoiceButton
title={l10n.t(LocalizationKey.dashboardHeaderHeaderCreateContent)}
choices={choiceOptions}
onClick={createContent}
disabled={!settings?.initialized}
/>
<RefreshDashboardData />
</div>
<Searchbox />
</div>
<div className={`px-4 flex flex-row items-center border-b justify-between border-[var(--frontmatter-border)]`}>
@@ -186,7 +189,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 +211,8 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
<Pagination totalPages={totalPages || 0} />
</div>
)}
<ActionsBar view={NavigationType.Contents} />
</>
)}
@@ -216,6 +221,8 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
<MediaHeaderTop />
<MediaHeaderBottom />
<ActionsBar view={NavigationType.Media} />
</>
)}

View File

@@ -63,7 +63,7 @@ export const Navigation: React.FunctionComponent<INavigationProps> = ({
}, [settings?.draftField?.type, tabInfo]);
return (
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
<nav className="flex-1 -mb-px flex space-x-2 xl:space-x-4" aria-label="Tabs">
{settings?.draftField?.type === 'boolean' ? (
tabs.map((tab) => (
<NavigationItem

View File

@@ -1,13 +1,13 @@
import { messageHandler } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import { ArrowsRightLeftIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { DashboardMessage } from '../../DashboardMessage';
import { SettingsSelector } from '../../state';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
import { MenuButton, MenuItem } from '../Menu';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
export interface IProjectSwitcherProps { }
@@ -32,8 +32,8 @@ export const ProjectSwitcher: React.FunctionComponent<IProjectSwitcherProps> = (
}
return (
<div className="flex items-center mr-4 z-[51]">
<Menu as="div" className="relative z-10 inline-block text-left">
<div className="mr-4 z-[51]">
<DropdownMenu>
<MenuButton
label={(
<div className="inline-flex items-center">
@@ -43,7 +43,7 @@ export const ProjectSwitcher: React.FunctionComponent<IProjectSwitcherProps> = (
)}
title={crntProject} />
<MenuItems disablePopper>
<DropdownMenuContent>
{projects.map((p) => (
<MenuItem
key={p.name}
@@ -53,8 +53,8 @@ export const ProjectSwitcher: React.FunctionComponent<IProjectSwitcherProps> = (
onClick={(value) => setProject(p.name)}
/>
))}
</MenuItems>
</Menu>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div >
);
};

View File

@@ -1,5 +1,4 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
@@ -18,6 +17,7 @@ import {
} from '../../state';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { ArrowClockwiseIcon } from '../../../components/icons/ArrowClockwiseIcon';
export interface IRefreshDashboardDataProps { }
@@ -62,11 +62,11 @@ export const RefreshDashboardData: React.FunctionComponent<IRefreshDashboardData
return (
<button
className={`mr-2 text-[var(--vscode-foreground)] hover:text-[var(--vscode-textLink-foreground)]`}
className={`mr-2 text-[var(--frontmatter-text)] hover:text-[var(--vscode-textLink-foreground)]`}
title={l10n.t(LocalizationKey.dashboardHeaderRefreshDashboardLabel)}
onClick={refresh}
>
<ArrowPathIcon className={`h-5 w-5`} />
<ArrowClockwiseIcon className={`h-5 w-5`} />
<span className="sr-only">{l10n.t(LocalizationKey.dashboardHeaderRefreshDashboardLabel)}</span>
</button>
);

View File

@@ -1,9 +1,8 @@
import { MagnifyingGlassIcon, XCircleIcon } from '@heroicons/react/24/solid';
import { MagnifyingGlassIcon } from '@heroicons/react/24/solid';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebounce } from '../../../hooks/useDebounce';
import { SearchAtom, SearchReadyAtom } from '../../state';
import { RefreshDashboardData } from './RefreshDashboardData';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { TextField } from '../Common/TextField';
@@ -40,7 +39,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)}
@@ -58,8 +57,6 @@ export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({
onReset={reset}
/>
</div>
<RefreshDashboardData />
</div>
);
};

View File

@@ -1,5 +1,4 @@
import { Messenger, messageHandler } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { ExtensionState } from '../../../constants';
@@ -9,10 +8,11 @@ import { DashboardMessage } from '../../DashboardMessage';
import { NavigationType } from '../../models';
import { SortingOption } from '../../models/SortingOption';
import { SearchSelector, SettingsSelector, SortingAtom } from '../../state';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
import { MenuButton, MenuItem } from '../Menu';
import { Sorting as SortingHelpers } from '../../../helpers/Sorting';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
export interface ISortingProps {
disableCustomSorting?: boolean;
@@ -177,26 +177,24 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({
}
return (
<div className="flex items-center">
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton
label={l10n.t(LocalizationKey.dashboardHeaderSortingLabel)}
title={crntSort?.title || crntSort?.name || ''}
disabled={!!searchValue}
/>
<DropdownMenu>
<MenuButton
label={l10n.t(LocalizationKey.dashboardHeaderSortingLabel)}
title={crntSort?.title || crntSort?.name || ''}
disabled={!!searchValue}
/>
<MenuItems widthClass="w-48" disablePopper>
{allOptions.map((option) => (
<MenuItem
key={option.id}
title={option.title || option.name}
value={option}
isCurrent={option.id === crntSort.id}
onClick={(value) => updateSorting(value)}
/>
))}
</MenuItems>
</Menu>
</div>
<DropdownMenuContent>
{allOptions.map((option) => (
<MenuItem
key={option.id}
title={option.title || option.name}
value={option}
isCurrent={option.id === crntSort.id}
onClick={(value) => updateSorting(value)}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -18,15 +18,15 @@ export const SyncButton: React.FunctionComponent<ISyncButtonProps> = (
const [isSyncing, setIsSyncing] = useState(false);
const pull = () => {
Messenger.send(GeneralCommands.toVSCode.gitSync);
Messenger.send(GeneralCommands.toVSCode.git.sync);
};
const messageListener = (message: MessageEvent<EventData<any>>) => {
const { command } = message.data;
if (command === GeneralCommands.toWebview.gitSyncingStart) {
if (command === GeneralCommands.toWebview.git.syncingStart) {
setIsSyncing(true);
} else if (command === GeneralCommands.toWebview.gitSyncingEnd) {
} else if (command === GeneralCommands.toWebview.git.syncingEnd) {
setIsSyncing(false);
}
};

View File

@@ -1,6 +1,7 @@
import * as React from 'react';
import { useLocation } from 'react-router-dom';
import { NavigationType } from '../../models';
import { cn } from '../../../utils/cn';
export interface ITabProps {
navigationType: NavigationType;
@@ -16,11 +17,11 @@ export const Tab: React.FunctionComponent<ITabProps> = ({
return (
<button
className={`h-full flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:border-[var(--vscode-tab-activeForeground)] hover:text-[var(--vscode-tab-activeForeground)] ${location.pathname === `/${navigationType}`
className={cn(`h-full flex items-center py-2 px-1 text-sm font-medium text-center border-b-2 border-transparent hover:text-[var(--vscode-tab-activeForeground)] ${location.pathname === `/${navigationType}`
?
`text-[var(--vscode-tab-activeForeground)] border-[var(--vscode-tab-activeForeground)]` :
`text-[var(--vscode-tab-inactiveForeground)]`
}`}
`text-[var(--frontmatter-nav-active)] border-[var(--frontmatter-nav-active)]` :
`text-[var(--frontmatter-nav-inactive)]`
}`)}
type="button"
role="tab"
aria-controls="profile"

View File

@@ -1,14 +1,14 @@
import { CircleStackIcon, PhotoIcon, ScissorsIcon, TagIcon } from '@heroicons/react/24/outline';
import { PhotoIcon, ScissorsIcon, TagIcon, CircleStackIcon } from '@heroicons/react/24/solid';
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { FeatureFlag } from '../../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../../constants';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { NavigationType } from '../../models';
import { ModeAtom } from '../../state';
import { Tab } from './Tab';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { PageIcon } from '../../../panelWebView/components/Icons';
export interface ITabsProps {
onNavigate: (navigationType: NavigationType) => void;
@@ -21,42 +21,42 @@ export const Tabs: React.FunctionComponent<ITabsProps> = ({
return (
<ul
className="flex items-center justify-start h-full"
className="flex items-center justify-start h-full space-x-4"
data-tabs-toggle="#myTabContent"
role="tablist"
>
<li className="mr-2" role="presentation">
<li role="presentation">
<Tab navigationType={NavigationType.Contents} onNavigate={onNavigate}>
<MarkdownIcon className={`h-6 w-auto mr-2`} />
<PageIcon className={`h-4 w-auto mr-2`} />
<span>{l10n.t(LocalizationKey.dashboardHeaderTabsContents)}</span>
</Tab>
</li>
<li className="mr-2" role="presentation">
<li role="presentation">
<Tab navigationType={NavigationType.Media} onNavigate={onNavigate}>
<PhotoIcon className={`h-6 w-auto mr-2`} />
<PhotoIcon className={`h-4 w-auto mr-2`} />
<span>{l10n.t(LocalizationKey.dashboardHeaderTabsMedia)}</span>
</Tab>
</li>
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.snippets.view}>
<li className="mr-2" role="presentation">
<li role="presentation">
<Tab navigationType={NavigationType.Snippets} onNavigate={onNavigate}>
<ScissorsIcon className={`h-6 w-auto mr-2`} />
<ScissorsIcon className={`h-4 w-auto mr-2`} />
<span>{l10n.t(LocalizationKey.dashboardHeaderTabsSnippets)}</span>
</Tab>
</li>
</FeatureFlag>
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.data.view}>
<li className="mr-2" role="presentation">
<li role="presentation">
<Tab navigationType={NavigationType.Data} onNavigate={onNavigate}>
<CircleStackIcon className={`h-6 w-auto mr-2`} />
<CircleStackIcon className={`h-4 w-auto mr-2`} />
<span>{l10n.t(LocalizationKey.dashboardHeaderTabsData)}</span>
</Tab>
</li>
</FeatureFlag>
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.taxonomy.view}>
<li className="mr-2" role="presentation">
<li role="presentation">
<Tab navigationType={NavigationType.Taxonomy} onNavigate={onNavigate}>
<TagIcon className={`h-6 w-auto mr-2`} />
<TagIcon className={`h-4 w-auto mr-2`} />
<span>{l10n.t(LocalizationKey.dashboardHeaderTabsTaxonomies)}</span>
</Tab>
</li>

View File

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

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { CustomScript, ScriptType } from '../../../models';
import { runCustomScript } from '../../utils';
import { CommandLineIcon } from '@heroicons/react/24/outline';
import { CommandLineIcon as CommandLineIconSolid } from '@heroicons/react/24/solid';
import { LocalizationKey } from '../../../localization';
export interface ICustomActionsProps {
filePath: string;
scripts?: CustomScript[];
showTrigger?: boolean;
}
export const CustomActions: React.FunctionComponent<ICustomActionsProps> = ({
filePath,
scripts,
showTrigger = false,
}: React.PropsWithChildren<ICustomActionsProps>) => {
const customActions = React.useMemo(() => {
return (scripts || [])
.filter((script) => script.type === ScriptType.MediaFile && !script.hidden)
.map((script) => (
<DropdownMenuItem
key={script.title}
onClick={() => runCustomScript(script, filePath)}
>
<CommandLineIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{script.title}</span>
</DropdownMenuItem>
));
}, [scripts]);
if (showTrigger) {
return (
<DropdownMenu>
<DropdownMenuTrigger
title={l10n.t(LocalizationKey.commonOpenCustomActions)}
className='px-2 text-[var(--frontmatter-secondary-text)] hover:text-[var(--frontmatter-button-hoverBackground)] focus-visible:outline-none'>
<span className="sr-only">{l10n.t(LocalizationKey.commonOpenCustomActions)}</span>
<CommandLineIconSolid className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{customActions}
</DropdownMenuContent>
</DropdownMenu>
);
}
return <>{customActions}</>;
};

View File

@@ -0,0 +1,227 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { DetailsInput } from './DetailsInput';
import { LocalizationKey } from '../../../localization';
import { DEFAULT_MEDIA_CONTENT_TYPE, MediaInfo, UnmappedMedia } from '../../../models';
import { useCallback, useEffect, useMemo } from 'react';
import { Messenger, messageHandler } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { basename } from 'path';
import { useRecoilValue } from 'recoil';
import { PageSelector, SelectedMediaFolderSelector, SettingsAtom } from '../../state';
export interface IDetailsFormProps {
media: MediaInfo;
isImageFile: boolean;
isVideoFile: boolean;
onDismiss: () => void;
}
export const DetailsForm: React.FunctionComponent<IDetailsFormProps> = ({
media,
isImageFile,
isVideoFile,
onDismiss,
}: React.PropsWithChildren<IDetailsFormProps>) => {
const settings = useRecoilValue(SettingsAtom);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const page = useRecoilValue(PageSelector);
const [filename, setFilename] = React.useState<string>(media.filename);
const [unmapped, setUnmapped] = React.useState<UnmappedMedia[]>([]);
const [metadata, setMetadata] = React.useState<{ [fieldName: string]: string }>({});
const fileInfo = useMemo(() => {
const fileInfo = filename ? basename(filename).split('.') : null;
const extension = fileInfo?.pop();
const name = fileInfo?.join('.');
return { name, extension };
}, [filename]);
const fields = useMemo(() => {
const contentType = settings?.media.contentTypes.find((c) => c.fileTypes?.map(t => t.toLowerCase()).includes(fileInfo.extension as string)) || DEFAULT_MEDIA_CONTENT_TYPE;
return contentType.fields;
}, [fileInfo, settings?.media.contentTypes]);
const updateMetadata = useCallback((fieldName: string, value: string) => {
setMetadata(prevMetadata => ({
...prevMetadata,
[fieldName]: value
}));
}, [metadata]);
const remapMetadata = useCallback((item: UnmappedMedia) => {
Messenger.send(DashboardMessage.remapMediaMetadata, {
file: media.fsPath,
unmappedItem: item,
folder: selectedFolder,
page
});
onDismiss();
}, [media, selectedFolder, page]);
const onSubmitMetadata = useCallback(() => {
Messenger.send(DashboardMessage.updateMediaMetadata, {
file: media.fsPath,
filename,
page,
folder: selectedFolder,
metadata,
});
onDismiss();
}, [media, filename, metadata, selectedFolder, page, onDismiss]);
const formFields = useMemo(() => {
return fields.map((field) => {
if (field.name === "title") {
return (
<div key="title">
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonTitle)}
</label>
<div className="mt-1">
<DetailsInput name={`title`} value={metadata?.title || ""} onChange={(e) => updateMetadata("title", e)} />
</div>
</div>
);
}
if (field.name === "caption") {
if (isImageFile || isVideoFile) {
return (
<div key="caption">
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonCaption)}
</label>
<div className="mt-1">
<DetailsInput name={`caption`} value={metadata?.caption || ""} onChange={(e) => updateMetadata("caption", e)} isTextArea />
</div>
</div>
)
} else {
return null;
}
}
if (field.name === "alt") {
if (isImageFile) {
return (
<div key="alt">
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonAlt)}
</label>
<div className="mt-1">
<DetailsInput name={`alt`} value={metadata?.alt || ""} onChange={(e) => updateMetadata("alt", e)} isTextArea />
</div>
</div>
)
} else {
return null;
}
}
return (
<div key={field.name}>
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{field.title || field.name}
</label>
<div className="mt-1">
<DetailsInput name={field.name} value={metadata[field.name] || ""} onChange={(e) => updateMetadata(field.name, e)} isTextArea={!field.single} />
</div>
</div>
);
});
}, [fields, metadata, updateMetadata]);
useEffect(() => {
if (fields && media.metadata && fileInfo?.extension) {
const metadataFields: { [fieldName: string]: string } = {};
fields.forEach((field) => {
metadataFields[field.name] = (media.metadata[field.name] || '') as string;
});
setMetadata(metadataFields);
}
}, [fileInfo, media.metadata, fields]);
useEffect(() => {
messageHandler.request<UnmappedMedia[]>(DashboardMessage.getUnmappedMedia, media.filename).then((result) => {
setUnmapped(result);
});
}, [media.filename]);
return (
<>
<h3 className={`text-base text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelTitle)}
</h3>
{
unmapped && unmapped.length > 0 && (
<div className="flex flex-col py-3 space-y-3">
<p className={`text-sm my-3 font-medium text-[var(--vscode-editor-foreground)] opacity-90`}>
{l10n.t(LocalizationKey.dashboardMediaDetailsSlideOverUnmappedDescription)}
</p>
<ul className='pl-4'>
{
unmapped.map((item) => (
<li className='list-disc'>
<button
key={item.file}
className='text-left hover:text-[var(--frontmatter-link-hover)]'
onClick={() => remapMetadata(item)}>
{item.file}{item.metadata.title ? ` (${item.metadata.title})` : ''}
</button>
</li>
))
}
</ul>
</div>
)
}
<p className={`text-sm my-3 font-medium text-[var(--vscode-editor-foreground)] opacity-90`}>
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelDescription)}
</p>
<div className="flex flex-col py-3 space-y-3">
<div>
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelFieldFileName)}
</label>
<div className="relative mt-1">
<DetailsInput name={`filename`} value={fileInfo.name || ""} onChange={(e) => setFilename(`${e}.${fileInfo.extension}`)} />
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className={`sm:text-sm placeholder-[var(--vscode-input-placeholderForeground)]`}>.{fileInfo?.extension}</span>
</div>
</div>
</div>
{formFields}
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
className={`w-full inline-flex justify-center rounded border-transparent shadow-sm px-4 py-2 text-base font-medium sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-30 bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] text-[var(--vscode-button-foreground)] outline-[var(--vscode-focusBorder)] outline-1`}
onClick={onSubmitMetadata}
disabled={!filename}
>
{l10n.t(LocalizationKey.commonSave)}
</button>
<button
type="button"
className={`mt-3 w-full inline-flex justify-center rounded shadow-sm px-4 py-2 text-base font-medium focus:outline-none sm:mt-0 sm:w-auto sm:text-sm bg-[var(--vscode-button-secondaryBackground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)] text-[var(--vscode-button-secondaryForeground)]`}
onClick={onDismiss}
>
{l10n.t(LocalizationKey.commonCancel)}
</button>
</div>
</>
);
};

View File

@@ -10,7 +10,7 @@ export const DetailsItem: React.FunctionComponent<IDetailsItemProps> = ({ title,
<>
<div className="py-3 flex justify-between text-sm font-medium">
<dt className={`text-[var(--vscode-editor-foreground)]`}>{title}</dt>
<dd className={`text-right text-[var(--vscode-foreground)]`}>
<dd className={`text-right text-[var(--frontmatter-text)]`}>
{details}
</dd>
</div>

View File

@@ -1,19 +1,17 @@
import { Dialog, Transition } from '@headlessui/react';
import { PencilSquareIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { format } from 'date-fns';
import { basename } from 'path';
import * as React from 'react';
import { Fragment, useCallback, useMemo } from 'react';
import { Fragment, useMemo } from 'react';
import { DateHelper } from '../../../helpers/DateHelper';
import { MediaInfo, UnmappedMedia } from '../../../models';
import { Messenger, messageHandler } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { useRecoilValue } from 'recoil';
import { PageSelector, SelectedMediaFolderSelector } from '../../state';
import { DEFAULT_MEDIA_CONTENT_TYPE, MediaInfo } from '../../../models';
import { DetailsItem } from './DetailsItem';
import { DetailsInput } from './DetailsInput';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { DetailsForm } from './DetailsForm';
import { useRecoilValue } from 'recoil';
import { SettingsAtom } from '../../state';
import { basename } from 'path';
export interface IDetailsSlideOverProps {
imgSrc: string;
@@ -42,62 +40,56 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
isImageFile,
isVideoFile
}: React.PropsWithChildren<IDetailsSlideOverProps>) => {
const [filename, setFilename] = React.useState<string>(media.filename);
const [caption, setCaption] = React.useState<string | undefined>(media.caption);
const [title, setTitle] = React.useState<string | undefined>(media.title);
const [unmapped, setUnmapped] = React.useState<UnmappedMedia[]>([]);
const [alt, setAlt] = React.useState(media.alt);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const page = useRecoilValue(PageSelector);
const settings = useRecoilValue(SettingsAtom);
const createdDate = useMemo(() => DateHelper.tryParse(media.ctime), [media]);
const modifiedDate = useMemo(() => DateHelper.tryParse(media.mtime), [media]);
const fileInfo = filename ? basename(filename).split('.') : null;
const extension = fileInfo?.pop();
const name = fileInfo?.join('.');
const extension = useMemo(() => {
const fileInfo = media.filename ? basename(media.filename).split('.') : null;
const extension = fileInfo?.pop();
return extension;
}, [media.filename]);
const onSubmitMetadata = useCallback(() => {
Messenger.send(DashboardMessage.updateMediaMetadata, {
file: media.fsPath,
filename,
caption,
alt,
title,
folder: selectedFolder,
page
});
onEditClose();
}, [media, filename, caption, alt, title, selectedFolder, page]);
const remapMetadata = useCallback((item: UnmappedMedia) => {
Messenger.send(DashboardMessage.remapMediaMetadata, {
file: media.fsPath,
unmappedItem: item,
folder: selectedFolder,
page
});
onEditClose();
}, [media, filename, caption, alt, title, selectedFolder, page]);
React.useEffect(() => {
setTitle(media.title);
setAlt(media.alt);
setCaption(media.caption);
setFilename(media.filename);
}, [media]);
React.useEffect(() => {
if (showForm) {
messageHandler.request<UnmappedMedia[]>(DashboardMessage.getUnmappedMedia, filename).then((result) => {
setUnmapped(result);
});
} else {
setUnmapped([]);
const fields = useMemo(() => {
if (extension) {
const contentType = settings?.media.contentTypes.find((c) => c.fileTypes?.map(t => t.toLowerCase()).includes(extension as string)) || DEFAULT_MEDIA_CONTENT_TYPE;
return contentType.fields;
}
}, [showForm, filename]);
}, [extension, settings?.media.contentTypes]);
const detailItems = useMemo(() => {
const items = [];
items.push(
<DetailsItem key="filename" title={l10n.t(LocalizationKey.dashboardMediaMetadataPanelFieldFileName)} details={media.filename} />
);
fields?.forEach((field) => {
if (field.name === "title") {
items.push(
<DetailsItem key="title" title={l10n.t(LocalizationKey.dashboardMediaCommonTitle)} details={media.metadata.title || ""} />
);
} else if (field.name === "caption") {
if (isImageFile) {
items.push(
<DetailsItem key="caption" title={l10n.t(LocalizationKey.dashboardMediaCommonCaption)} details={media.metadata.caption || ""} />
);
}
} else if (field.name === "alt") {
if (isImageFile) {
items.push(
<DetailsItem key="alt" title={l10n.t(LocalizationKey.dashboardMediaCommonAlt)} details={media.metadata.alt || ""} />
);
}
} else {
items.push(
<DetailsItem key={field.name} title={field.title || field.name} details={(media.metadata[field.name] || "") as string} />
);
}
});
return items;
}, [fields, media.metadata]);
return (
<Transition.Root show={true} as={Fragment}>
@@ -136,7 +128,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
</div>
<div className="relative mt-6 flex-1 px-4 sm:px-6">
<div className="absolute inset-0 px-4 sm:px-6 space-y-8">
<div className="space-y-8">
<div>
{(isImageFile || isVideoFile) && (
<div className={`block w-full aspect-w-10 aspect-h-7 overflow-hidden border rounded border-[var(--frontmatter-border)] bg-[var(--vscode-editor-background)]`}>
@@ -155,7 +147,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
)}
<div className="mt-4 flex items-start justify-between">
<div>
<h2 className={`text-lg font-medium text-[var(--vscode-foreground)]`}>
<h2 className={`text-lg font-medium text-[var(--frontmatter-text)]`}>
{media.filename}
</h2>
<p className={`text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
@@ -168,106 +160,16 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
<div>
{/* EDIT METADATA FORM */}
{showForm && (
<>
<h3 className={`text-base text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelTitle)}
</h3>
{
unmapped && unmapped.length > 0 && (
<div className="flex flex-col py-3 space-y-3">
<p className={`text-sm my-3 font-medium text-[var(--vscode-editor-foreground)] opacity-90`}>
{l10n.t(LocalizationKey.dashboardMediaDetailsSlideOverUnmappedDescription)}
</p>
<ul className='pl-4'>
{
unmapped.map((item) => (
<li className='list-disc'>
<button
key={item.file}
className='text-left hover:text-[var(--frontmatter-link-hover)]'
onClick={() => remapMetadata(item)}>
{item.file}{item.metadata.title ? ` (${item.metadata.title})` : ''}
</button>
</li>
))
}
</ul>
</div>
)
}
<p className={`text-sm my-3 font-medium text-[var(--vscode-editor-foreground)] opacity-90`}>
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelDescription)}
</p>
<div className="flex flex-col py-3 space-y-3">
<div>
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelFieldFileName)}
</label>
<div className="relative mt-1">
<DetailsInput name={`filename`} value={name || ""} onChange={(e) => setFilename(`${e}.${extension}`)} />
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className={`sm:text-sm placeholder-[var(--vscode-input-placeholderForeground)]`}>.{extension}</span>
</div>
</div>
</div>
<div>
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonTitle)}
</label>
<div className="mt-1">
<DetailsInput name={`title`} value={title || ""} onChange={(e) => setTitle(e)} />
</div>
</div>
{(isImageFile || isVideoFile) && (
<div>
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonCaption)}
</label>
<div className="mt-1">
<DetailsInput name={`caption`} value={caption || ""} onChange={(e) => setCaption(e)} isTextArea />
</div>
</div>
)}
{isImageFile && (
<div>
<label className={`block text-sm font-medium text-[var(--vscode-editor-foreground)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonAlt)}
</label>
<div className="mt-1">
<DetailsInput name={`alt`} value={alt || ""} onChange={(e) => setAlt(e)} isTextArea />
</div>
</div>
)}
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
className={`w-full inline-flex justify-center rounded border-transparent shadow-sm px-4 py-2 text-base font-medium sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-30 bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] text-[var(--vscode-button-foreground)] outline-[var(--vscode-focusBorder)] outline-1`}
onClick={onSubmitMetadata}
disabled={!filename}
>
{l10n.t(LocalizationKey.commonSave)}
</button>
<button
type="button"
className={`mt-3 w-full inline-flex justify-center rounded shadow-sm px-4 py-2 text-base font-medium focus:outline-none sm:mt-0 sm:w-auto sm:text-sm bg-[var(--vscode-button-secondaryBackground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)] text-[var(--vscode-button-secondaryForeground)]`}
onClick={onEditClose}
>
{l10n.t(LocalizationKey.commonCancel)}
</button>
</div>
</>
<DetailsForm
media={media}
isImageFile={isImageFile}
isVideoFile={isVideoFile}
onDismiss={onEditClose} />
)}
{!showForm && (
<>
<h3 className={`text-base flex items-center text-[var(--vscode-foreground)]`}>
<h3 className={`text-base flex items-center text-[var(--frontmatter-text)]`}>
<span>{l10n.t(LocalizationKey.dashboardMediaMetadataPanelFormMetadataTitle)}</span>
<button onClick={onEdit}>
<PencilSquareIcon className="w-4 h-4 ml-2" aria-hidden="true" />
@@ -275,15 +177,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
</button>
</h3>
<dl className={`mt-2 border-t border-b divide-y border-[var(--frontmatter-border)] divide-[var(--frontmatter-border)]`}>
<DetailsItem title={l10n.t(LocalizationKey.dashboardMediaMetadataPanelFieldFileName)} details={media.filename} />
<DetailsItem title={l10n.t(LocalizationKey.dashboardMediaCommonTitle)} details={media.title || ""} />
{isImageFile && (
<>
<DetailsItem title={l10n.t(LocalizationKey.dashboardMediaCommonCaption)} details={media.caption || ''} />
<DetailsItem title={l10n.t(LocalizationKey.dashboardMediaCommonAlt)} details={media.alt || ''} />
</>
)}
{detailItems}
</dl>
</>
)}
@@ -291,7 +185,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
{!showForm && (
<div>
<h3 className={`text-base text-[var(--vscode-foreground)]`}>
<h3 className={`text-base text-[var(--frontmatter-text)]`}>
{l10n.t(LocalizationKey.dashboardMediaMetadataPanelFormInformationTitle)}
</h3>
<dl className={`mt-2 border-t border-b divide-y border-[var(--frontmatter-border)] divide-[var(--frontmatter-border)]`}>

View File

@@ -5,7 +5,6 @@ import { DashboardMessage } from '../../DashboardMessage';
import {
AllContentFoldersAtom,
AllStaticFoldersAtom,
SelectedMediaFolderAtom,
SettingsSelector,
ViewDataSelector
} from '../../state';
@@ -18,13 +17,15 @@ import { extname } from 'path';
import { parseWinPath } from '../../../helpers/parseWinPath';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import useMediaFolder from '../../hooks/useMediaFolder';
import { RefreshDashboardData } from '../Header/RefreshDashboardData';
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 +91,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 +108,7 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
}
return (
<div className="flex flex-1 justify-end">
<div className="flex flex-1 justify-start space-x-2">
{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`}
@@ -117,6 +118,8 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
<FolderPlusIcon className={`mr-2 h-6 w-6`} />
<span className={``}>{l10n.t(LocalizationKey.dashboardMediaFolderCreationFolderCreate)}</span>
</button>
<RefreshDashboardData />
</div>
);
};

View File

@@ -1,8 +1,9 @@
import { FolderIcon } from '@heroicons/react/24/solid';
import { basename, join } from 'path';
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { SelectedMediaFolderAtom } from '../../state';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import useMediaFolder from '../../hooks/useMediaFolder';
export interface IFolderItemProps {
folder: string;
@@ -15,7 +16,7 @@ export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({
wsFolder,
staticFolder
}: React.PropsWithChildren<IFolderItemProps>) => {
const [, setSelectedFolder] = useRecoilState(SelectedMediaFolderAtom);
const { updateFolder } = useMediaFolder();
const relFolderPath = wsFolder ? folder.replace(wsFolder, '') : folder;
@@ -29,14 +30,14 @@ 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`} />
{isContentFolder && (
<span className={`font-extrabold absolute bottom-3 left-1/2 transform -translate-x-1/2 text-[var(--vscode-foreground)]`}>
<span className={`font-extrabold absolute bottom-3 left-1/2 transform -translate-x-1/2 text-[var(--frontmatter-text)]`}>
C
</span>
)}

View File

@@ -0,0 +1,111 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { QuickAction } from '../Menu';
import { LocalizationKey } from '../../../localization';
import { ClipboardIcon, CodeBracketIcon, EyeIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid';
import { useRecoilState } from 'recoil';
import { SelectedItemActionAtom } from '../../state';
import { CustomScript, MediaInfo, Snippet, ViewData } from '../../../models';
import { copyToClipboard } from '../../utils';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { CustomActions } from './CustomActions';
export interface IFooterActionsProps {
media: MediaInfo;
snippets: Snippet[];
relPath?: string;
viewData?: ViewData | undefined
scripts?: CustomScript[];
insertIntoArticle: () => void;
insertSnippet: () => void;
onDelete: () => void;
}
export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
relPath,
media,
snippets,
viewData,
scripts,
insertIntoArticle,
insertSnippet,
onDelete,
}: React.PropsWithChildren<IFooterActionsProps>) => {
const [, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
return (
<div className={`py-2 w-full flex items-center justify-evenly border-t border-t-[var(--frontmatter-border)] bg-[var(--frontmatter-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)]`}>
<QuickAction
title={l10n.t(LocalizationKey.dashboardMediaItemMenuItemView)}
className={`text-[var(--frontmatter-secondary-text)]`}
onClick={() => setSelectedItemAction({
path: media.fsPath,
action: 'view'
})}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
<span className='sr-only'>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemView)}</span>
</QuickAction>
<QuickAction
title={l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}
className={`text-[var(--frontmatter-secondary-text)]`}
onClick={() => setSelectedItemAction({
path: media.fsPath,
action: 'edit'
})}>
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
<span className='sr-only'>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}</span>
</QuickAction>
{viewData?.filePath ? (
<>
<QuickAction
title={
viewData.metadataInsert && viewData.fieldName
? l10n.t(LocalizationKey.dashboardMediaItemQuickActionInsertField, viewData.fieldName)
: l10n.t(LocalizationKey.dashboardMediaItemQuickActionInsertMarkdown)
}
className={`text-[var(--frontmatter-secondary-text)]`}
onClick={insertIntoArticle}
>
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
{viewData?.position && snippets.length > 0 && (
<QuickAction
title={l10n.t(LocalizationKey.commonInsertSnippet)}
className={`text-[var(--frontmatter-secondary-text)]`}
onClick={insertSnippet}>
<CodeBracketIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)}
</>
) : (
<>
{
relPath && (
<QuickAction
title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionCopyPath)}
className={`text-[var(--frontmatter-secondary-text)]`}
onClick={() => copyToClipboard(parseWinPath(relPath) || '')}>
<ClipboardIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)
}
</>
)}
<CustomActions
filePath={media.fsPath}
scripts={scripts}
showTrigger />
<QuickAction
title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionDelete)}
className={`text-[var(--frontmatter-secondary-text)] hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
);
};

View File

@@ -1,78 +1,67 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import {
ClipboardIcon,
CodeBracketIcon,
DocumentIcon,
EyeIcon,
MusicalNoteIcon,
PencilIcon,
PhotoIcon,
PlusIcon,
CommandLineIcon,
TrashIcon,
VideoCameraIcon
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';
import { CustomScript } from '../../../helpers/CustomScript';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { SnippetParser } from '../../../helpers/SnippetParser';
import { ScriptType, Snippet } from '../../../models';
import { MediaInfo } from '../../../models/MediaPaths';
import { DashboardMessage } from '../../DashboardMessage';
import {
LightboxAtom,
SelectedItemActionAtom,
SelectedMediaFolderSelector,
SettingsSelector,
ViewDataSelector
} from '../../state';
import { MenuItem, MenuItems } from '../Menu';
import { ActionMenuButton } from '../Menu/ActionMenuButton';
import { QuickAction } from '../Menu/QuickAction';
import { Alert } from '../Modals/Alert';
import { InfoDialog } from '../Modals/InfoDialog';
import { DetailsSlideOver } from './DetailsSlideOver';
import { usePopper } from 'react-popper';
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';
import { FooterActions } from './FooterActions';
export interface IItemProps {
media: MediaInfo;
}
export const Item: React.FunctionComponent<IItemProps> = ({
media
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 [caption, setCaption] = useState(media.caption);
const [alt, setAlt] = useState(media.alt);
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);
}, [media.fsPath, settings?.staticFolder, settings?.wsFolder]);
const hasViewData = useMemo(() => {
return viewData?.data?.filePath !== undefined;
}, [viewData]);
const [referenceElement, setReferenceElement] = useState<any>(null);
const [popperElement, setPopperElement] = useState<any>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: 'bottom-end',
strategy: 'fixed'
});
const mediaSnippets = useMemo(() => {
if (!settings?.snippets) {
return [];
@@ -88,57 +77,11 @@ 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 getRelPath = () => {
let relPath: string | undefined = '';
if (settings?.wsFolder && media.fsPath) {
const wsFolderParsed = parseWinPath(settings.wsFolder);
const mediaParsed = parseWinPath(media.fsPath);
relPath = mediaParsed.split(wsFolderParsed).pop();
// If the static folder is the root, we can just return the relative path
if (settings.staticFolder === "/") {
return relPath;
} else if (settings.staticFolder && relPath) {
const staticFolderParsed = parseWinPath(settings.staticFolder);
relPath = relPath.split(staticFolderParsed).pop();
}
}
return relPath;
};
const getFileName = () => {
return basename(parseWinPath(media.fsPath) || '');
};
const copyToClipboard = () => {
const relPath = getRelPath();
Messenger.send(DashboardMessage.copyToClipboard, parseWinPath(relPath) || '');
};
const runCustomScript = (script: CustomScript) => {
Messenger.send(DashboardMessage.runCustomScript, {
script,
path: media.fsPath
});
};
const insertToArticle = () => {
const relPath = getRelPath();
const insertIntoArticle = useCallback(() => {
if (viewData?.data?.type === 'file') {
Messenger.send(DashboardMessage.insertFile, {
relPath: parseWinPath(relPath) || '',
@@ -150,7 +93,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
position: viewData?.data?.position || null,
blockData:
typeof viewData?.data?.blockData !== 'undefined' ? viewData?.data?.blockData : undefined,
title: media.title
title: media.metadata.title
});
} else {
Messenger.send(DashboardMessage.insertMedia, {
@@ -163,12 +106,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({
position: viewData?.data?.position || null,
blockData:
typeof viewData?.data?.blockData !== 'undefined' ? viewData?.data?.blockData : undefined,
alt: alt || '',
caption: caption || '',
title: media.title || ''
alt: media.metadata.alt || '',
caption: media.metadata.caption || '',
title: media.metadata.title || ''
});
}
};
}, [media, settings, viewData, relPath]);
const insertSnippet = useCallback(() => {
if (mediaSnippets.length === 1) {
@@ -186,16 +129,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({
(snippet: Snippet) => {
setShowSnippetSelection(false);
const relPath = getRelPath();
const fieldData = {
mediaUrl: (parseWinPath(relPath) || '').replace(/ /g, '%20'),
alt: alt || '',
caption: caption || '',
title: media.title || '',
filename: basename(relPath || ''),
mediaWidth: media?.dimensions?.width?.toString() || '',
mediaHeight: media?.dimensions?.height?.toString() || ''
mediaHeight: media?.dimensions?.height?.toString() || '',
...media.metadata
};
if (!snippet.fields || snippet.fields.length === 0) {
@@ -215,7 +154,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
setMediaData(fieldData);
}
},
[alt, caption, media, settings, viewData, mediaSnippets]
[media, settings, viewData, mediaSnippets, relPath]
);
/**
@@ -223,8 +162,6 @@ export const Item: React.FunctionComponent<IItemProps> = ({
*/
const insertMediaSnippetToArticle = useCallback(
(output: string) => {
const relPath = getRelPath();
Messenger.send(DashboardMessage.insertMedia, {
relPath: parseWinPath(relPath) || '',
file: viewData?.data?.filePath,
@@ -233,20 +170,9 @@ export const Item: React.FunctionComponent<IItemProps> = ({
snippet: output
});
},
[viewData]
[viewData, relPath]
);
const deleteMedia = () => {
setShowAlert(true);
};
const revealMedia = () => {
Messenger.send(DashboardMessage.revealMedia, {
file: media.fsPath,
folder: selectedFolder
});
};
const confirmDeletion = () => {
Messenger.send(DashboardMessage.deleteMedia, {
file: media.fsPath,
@@ -254,98 +180,12 @@ 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 viewMediaDetails = () => {
setShowDetails(true);
};
const openLightbox = useCallback(() => {
if (isImageFile) {
if (isImage) {
setLightbox(media.vsPath || '');
}
}, [media.vsPath]);
const updateMetadata = () => {
setShowForm(true);
setShowDetails(true);
};
const customScriptActions = () => {
return (settings?.scripts || [])
.filter((script) => script.type === ScriptType.MediaFile && !script.hidden)
.map((script) => (
<MenuItem
key={script.title}
title={
<div className="flex items-center">
<CommandLineIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} />{' '}
<span>{script.title}</span>
</div>
}
onClick={() => runCustomScript(script)}
/>
));
};
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;
}, [media]);
const renderMediaIcon = useMemo(() => {
const path = media.fsPath;
const extension = path.split('.').pop();
@@ -358,15 +198,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}`} />;
}
@@ -378,18 +218,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" />
);
@@ -404,18 +244,6 @@ export const Item: React.FunctionComponent<IItemProps> = ({
setMediaData(undefined);
};
useEffect(() => {
if (media.alt !== alt) {
setAlt(media.alt);
}
}, [media.alt]);
useEffect(() => {
if (media.caption !== caption) {
setCaption(media.caption);
}
}, [media.caption]);
useEffect(() => {
const name = basename(parseWinPath(media.fsPath) || '');
if (name !== filename) {
@@ -431,9 +259,9 @@ export const Item: React.FunctionComponent<IItemProps> = ({
return (
<>
<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)]`}>
<li className={`group flex flex-col 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
@@ -446,6 +274,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`}
@@ -455,9 +286,9 @@ export const Item: React.FunctionComponent<IItemProps> = ({
} flex items-center justify-center`}
>
<button
title="Insert image"
title={l10n.t(LocalizationKey.dashboardMediaItemButtomInsertImage)}
className={`h-1/3 text-white hover:text-[var(--vscode-button-background)]`}
onClick={insertToArticle}
onClick={insertIntoArticle}
>
<PlusIcon className={`w-full h-full hover:drop-shadow-md `} aria-hidden="true" />
</button>
@@ -465,7 +296,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
{viewData?.data?.position && mediaSnippets.length > 0 && (
<div className={`h-full w-1/3 flex items-center justify-center`}>
<button
title="Insert snippet"
title={l10n.t(LocalizationKey.dashboardMediaItemButtomInsertSnippet)}
className={`h-1/3 text-white hover:text-[var(--vscode-button-background)]`}
onClick={insertSnippet}
>
@@ -476,188 +307,80 @@ export const Item: React.FunctionComponent<IItemProps> = ({
</button>
</div>
)}
<ItemSelection filePath={media.fsPath} />
</div>
)}
</button>
<div className={`relative py-4 pl-4 pr-12`}>
<div className={`group/actions absolute top-4 right-4 flex flex-col space-y-4`}>
<div className={`flex items-center border border-transparent rounded-full p-2 -mr-2 -mt-2 group-hover/actions:bg-[var(--vscode-sideBar-background)] group-hover/actions:border-[var(--frontmatter-border)]`}>
<Menu as="div" className="relative z-10 flex text-left">
<div className="hidden group-hover/actions:flex">
<QuickAction title="View media details" onClick={viewMediaDetails}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<QuickAction title="Edit metadata" onClick={updateMetadata}>
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
<div className={`relative py-4 pl-4 pr-12 grow`}>
<ItemMenu
media={media}
relPath={relPath}
selectedFolder={selectedFolder}
viewData={viewData?.data}
snippets={mediaSnippets}
scripts={settings?.scripts}
insertIntoArticle={insertIntoArticle}
showUpdateMedia={() => setSelectedItemAction({
path: media.fsPath,
action: 'edit'
})}
showMediaDetails={() => setSelectedItemAction({
path: media.fsPath,
action: 'view'
})}
processSnippet={processSnippet}
onDelete={() => setShowAlert(true)} />
{viewData?.data?.filePath ? (
<>
<QuickAction
title={
viewData.data.metadataInsert && viewData.data.fieldName
? l10n.t(LocalizationKey.dashboardMediaItemQuickActionInsertField, viewData.data.fieldName)
: l10n.t(LocalizationKey.dashboardMediaItemQuickActionInsertMarkdown)
}
onClick={insertToArticle}
>
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
{viewData?.data?.position && mediaSnippets.length > 0 && (
<QuickAction title={l10n.t(LocalizationKey.commonInsertSnippet)} onClick={insertSnippet}>
<CodeBracketIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)}
</>
) : (
<>
<QuickAction title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionCopyPath)} onClick={copyToClipboard}>
<ClipboardIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</>
)}
<QuickAction title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionDelete)} onClick={deleteMedia}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
<div ref={setReferenceElement} className={`flex`}>
<ActionMenuButton title={l10n.t(LocalizationKey.commonMenu)} />
</div>
<div
className="menu_items__wrapper z-20"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<MenuItems widthClass="w-40">
<MenuItem
title={
<div className="flex items-center">
<PencilIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} />{' '}
<span>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}</span>
</div>
}
onClick={updateMetadata}
/>
{viewData?.data?.filePath ? (
<>
<MenuItem
title={
<div className="flex items-center">
<PlusIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} />{' '}
<span>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemInsertImage)}</span>
</div>
}
onClick={insertToArticle}
/>
{viewData?.data?.position &&
mediaSnippets.length > 0 &&
mediaSnippets.map((snippet, idx) => (
<MenuItem
key={idx}
title={
<div className="flex items-center">
<CodeBracketIcon
className="mr-2 h-5 w-5 flex-shrink-0"
aria-hidden={true}
/>{' '}
<span>{snippet.title}</span>
</div>
}
onClick={() => processSnippet(snippet)}
/>
))}
{customScriptActions()}
</>
) : (
<>
<MenuItem
title={
<div className="flex items-center">
<ClipboardIcon
className="mr-2 h-5 w-5 flex-shrink-0"
aria-hidden={true}
/>{' '}
<span>{l10n.t(LocalizationKey.dashboardMediaItemQuickActionCopyPath)}</span>
</div>
}
onClick={copyToClipboard}
/>
{customScriptActions()}
</>
)}
<MenuItem
title={
<div className="flex items-center">
<EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} />{' '}
<span>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemRevealMedia)}</span>
</div>
}
onClick={revealMedia}
/>
<MenuItem
title={
<div className="flex items-center">
<TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} />{' '}
<span>{l10n.t(LocalizationKey.commonDelete)}</span>
</div>
}
onClick={deleteMedia}
/>
</MenuItems>
</div>
</Menu>
</div>
</div>
<p className={`text-sm font-bold pointer-events-none flex items-center break-all text-[var(--vscode-foreground)]}`}>
<p className={`text-sm font-bold pointer-events-none flex items-center break-all text-[var(--frontmatter-text)]`}>
{basename(parseWinPath(media.fsPath) || '')}
</p>
{!isImageFile && media.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`}>
<b className={`mr-2 text-[var(--frontmatter-text)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonTitle)}:
</b>
<span className={`block mt-1 text-xs text-[var(--vscode-foreground)]`}>{media.title}</span>
<span className={`block mt-1 text-xs text-[var(--frontmatter-secondary-text)]`}>{media.metadata.title}</span>
</p>
)}
{media.caption && (
{media.metadata.caption && (
<p className={`mt-2 text-xs font-medium pointer-events-none flex flex-col items-start`}>
<b className={`mr-2`}>
<b className={`mr-2 text-[var(--frontmatter-text)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonCaption)}:
</b>
<span className={`block mt-1 text-xs text-[var(--vscode-foreground)]`}>{media.caption}</span>
<span className={`block mt-1 text-xs text-[var(--frontmatter-secondary-text)]`}>{media.metadata.caption}</span>
</p>
)}
{!media.caption && media.alt && (
{!media.metadata.caption && media.metadata.alt && (
<p className={`mt-2 text-xs font-medium pointer-events-none flex flex-col items-start`}>
<b className={`mr-2`}>
<b className={`mr-2 text-[var(--frontmatter-text)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonAlt)}:
</b>
<span className={`block mt-1 text-xs text-[var(--vscode-foreground)]`}>{media.alt}</span>
<span className={`block mt-1 text-xs text-[var(--frontmatter-secondary-text)]`}>{media.metadata.alt}</span>
</p>
)}
{(media?.size || media?.dimensions) && (
<p className={`mt-2 text-xs font-medium pointer-events-none flex flex-col items-start`}>
<b className={`mr-1`}>
<b className={`mr-1 text-[var(--frontmatter-text)]`}>
{l10n.t(LocalizationKey.dashboardMediaCommonSize)}:
</b>
<span className={`block mt-1 text-xs text-[var(--vscode-foreground)]`}>
{getMediaDetails()}
<span className={`block mt-1 text-xs text-[var(--frontmatter-secondary-text)]`}>
{mediaDetails}
</span>
</p>
)}
</div>
<FooterActions
media={media}
relPath={relPath}
snippets={mediaSnippets}
viewData={viewData?.data}
scripts={settings?.scripts}
insertIntoArticle={insertIntoArticle}
insertSnippet={insertSnippet}
onDelete={() => setShowAlert(true)} />
</li>
{showSnippetSelection && (
@@ -682,29 +405,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({
</InfoDialog>
)}
{showDetails && (
<DetailsSlideOver
imgSrc={media.vsPath || ''}
size={getSize()}
dimensions={getDimensions()}
folder={getFolder()}
media={media}
showForm={showForm}
isImageFile={isImageFile}
isVideoFile={isVideoFile}
onEdit={() => setShowForm(true)}
onEditClose={() => setShowForm(false)}
onDismiss={() => {
setShowDetails(false);
setShowForm(false);
}}
/>
)}
{showAlert && (
<Alert
title={`${l10n.t(LocalizationKey.commonDelete)}: ${basename(parseWinPath(media.fsPath) || '')}`}
description={l10n.t(LocalizationKey.dashboardMediaItemAlertDeleteDescription, getFolder())}
description={l10n.t(LocalizationKey.dashboardMediaItemAlertDeleteDescription, mediaFolder)}
okBtnText={l10n.t(LocalizationKey.commonDelete)}
cancelBtnText={l10n.t(LocalizationKey.commonCancel)}
dismiss={() => setShowAlert(false)}

View File

@@ -0,0 +1,122 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { ClipboardIcon, CodeBracketIcon, EllipsisHorizontalIcon, EyeIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import { CustomScript, MediaInfo, Snippet, ViewData } from '../../../models';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { copyToClipboard } from '../../utils';
import { CustomActions } from './CustomActions';
export interface IItemMenuProps {
media: MediaInfo;
relPath?: string;
selectedFolder: string | null;
viewData?: ViewData;
snippets: Snippet[];
scripts?: CustomScript[];
insertIntoArticle: () => void;
showUpdateMedia: () => void;
showMediaDetails: () => void;
processSnippet: (snippet: Snippet) => void;
onDelete: () => void;
}
export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
media,
relPath,
selectedFolder,
viewData,
snippets,
scripts,
insertIntoArticle,
showUpdateMedia,
showMediaDetails,
processSnippet,
onDelete,
}: React.PropsWithChildren<IItemMenuProps>) => {
const onCopyToClipboard = React.useCallback(() => {
copyToClipboard(parseWinPath(relPath) || '');
}, [relPath]);
const revealMedia = React.useCallback(() => {
messageHandler.send(DashboardMessage.revealMedia, {
file: media.fsPath,
folder: selectedFolder
});
}, [selectedFolder]);
return (
<div className={`group/actions absolute top-4 right-4 flex flex-col space-y-4`}>
<div className={`flex items-center border border-transparent rounded-full p-1 -mr-2 -mt-1 group-hover/actions:bg-[var(--vscode-sideBar-background)] group-hover/actions:border-[var(--frontmatter-border)]`}>
<div className="relative z-10 flex text-left">
<DropdownMenu>
<DropdownMenuTrigger className='text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]'>
<span className="sr-only">{l10n.t(LocalizationKey.commonMenu)}</span>
<EllipsisHorizontalIcon className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={showMediaDetails}>
<EyeIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonView)}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={showUpdateMedia}>
<PencilIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}</span>
</DropdownMenuItem>
{
viewData?.filePath ? (
<>
<DropdownMenuItem onClick={insertIntoArticle}>
<PlusIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemInsertImage)}</span>
</DropdownMenuItem>
{
viewData?.position &&
snippets.length > 0 &&
snippets.map((snippet, idx) => (
<DropdownMenuItem key={idx} onClick={() => processSnippet(snippet)}>
<CodeBracketIcon
className="mr-2 h-4 w-4"
aria-hidden={true}
/>
<span>{snippet.title}</span>
</DropdownMenuItem>
))
}
</>
) : (
<DropdownMenuItem onClick={onCopyToClipboard}>
<ClipboardIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardMediaItemQuickActionCopyPath)}</span>
</DropdownMenuItem>
)
}
<CustomActions
filePath={media.fsPath}
scripts={scripts} />
<DropdownMenuItem onClick={revealMedia}>
<EyeIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemRevealMedia)}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onDelete} className={`focus:bg-[var(--vscode-statusBarItem-errorBackground)] focus:text-[var(--vscode-statusBarItem-errorForeground)]`}>
<TrashIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonDelete)}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
);
};

View File

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

View File

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

View File

@@ -0,0 +1,67 @@
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 [crntPath, setCrntPath] = useState<string>('');
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);
const onDismiss = () => {
setShowDetails(false);
setShowForm(false);
setMedia(undefined);
};
useEffect(() => {
if (selectedItemAction && selectedItemAction.path) {
const mediaFile = allMedia.find((m) => m.fsPath === selectedItemAction.path);
setMedia(mediaFile);
setCrntPath(selectedItemAction.path);
if (selectedItemAction.action === 'edit') {
setShowForm(true);
setShowDetails(true);
} else if (selectedItemAction.action === 'view') {
setShowForm(false);
setShowDetails(true);
}
setSelectedItemAction(undefined);
} else if (crntPath) {
const mediaFile = allMedia.find((m) => m.fsPath === crntPath);
setMedia(mediaFile);
}
}, [allMedia, selectedItemAction, crntPath]);
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={onDismiss}
/>
);
}
return null;
};

View File

@@ -33,8 +33,8 @@ export const MediaSnippetForm: React.FunctionComponent<IMediaSnippetFormProps> =
return (
<SnippetSlideOver
title={l10n.t(LocalizationKey.dashboardMediaMediaSnippetFormFormDialogTitle, media.title || media.filename)}
description={l10n.t(LocalizationKey.dashboardMediaMediaSnippetFormFormDialogDescription, media.title || media.filename)}
title={l10n.t(LocalizationKey.dashboardMediaMediaSnippetFormFormDialogTitle, media.metadata.title || media.filename)}
description={l10n.t(LocalizationKey.dashboardMediaMediaSnippetFormFormDialogDescription, media.metadata.title || media.filename)}
isSaveDisabled={false}
trigger={insertToArticle}
dismiss={onDismiss}

View File

@@ -1,4 +1,3 @@
import { Menu } from '@headlessui/react';
import { EllipsisVerticalIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
@@ -14,15 +13,14 @@ export const ActionMenuButton: React.FunctionComponent<IActionMenuButtonProps> =
ref
}: React.PropsWithChildren<IActionMenuButtonProps>) => {
return (
<Menu.Button
<button
ref={ref || null}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => e.stopPropagation()}
disabled={disabled}
className={`group inline-flex justify-center text-sm font-medium text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)] ${disabled ? 'opacity-50' : ''
}`}
className={`group inline-flex justify-center text-sm font-medium text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)] ${disabled ? 'opacity-50' : ''}`}
>
<span className="sr-only">{title}</span>
<EllipsisVerticalIcon className="w-4 h-4" aria-hidden="true" />
</Menu.Button>
</button>
);
};

View File

@@ -1,6 +1,6 @@
import { Menu } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/24/solid';
import * as React from 'react';
import { DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
export interface IMenuButtonProps {
label: string | JSX.Element;
@@ -14,19 +14,17 @@ export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({
disabled
}: React.PropsWithChildren<IMenuButtonProps>) => {
return (
<div className={`group flex items-center ${disabled ? 'opacity-50' : ''}`}>
<div className={`mr-2 font-medium flex items-center text-[var(--vscode-tab-inactiveForeground)]`}>{label}:</div>
<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>
<Menu.Button
disabled={disabled}
className={`group inline-flex justify-center text-sm font-medium text-[var(--vscode-textLink-foreground)] hover:text-[var(--vscode-textLink-activeForeground)]`}
>
{title}
<ChevronDownIcon
className={`flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-[var(--vscode-textLink-foreground)] group-hover:text-[var(--vscode-textLink-activeForeground)]`}
aria-hidden="true"
/>
</Menu.Button>
<DropdownMenuTrigger
className='text-[var(--vscode-textLink-foreground)] hover:text-[var(--vscode-textLink-activeForeground)] flex items-center focus:outline-none'
disabled={disabled}>
<span>{title}</span>
<ChevronDownIcon className={`-mr-1 ml-1 h-4 w-4`} aria-hidden="true" />
</DropdownMenuTrigger>
</div>
);
};

View File

@@ -1,31 +1,30 @@
import { Menu } from '@headlessui/react';
import * as React from 'react';
import { DropdownMenuItem } from '../../../components/shadcn/Dropdown';
export interface IMenuItemProps {
title: JSX.Element | string;
value?: any;
isCurrent?: boolean;
className?: string;
disabled?: boolean;
onClick: (value: any, e: React.MouseEvent<HTMLButtonElement>) => void;
onClick: (value: any, e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
}
export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({
title,
value,
isCurrent,
className,
disabled,
onClick
}: React.PropsWithChildren<IMenuItemProps>) => {
return (
<Menu.Item>
<button
disabled={disabled}
onClick={(e) => onClick(value, e)}
className={`${!isCurrent ? `font-normal` : `font-bold`
} block px-4 py-2 text-sm w-full text-left disabled:opacity-50 text-[var(--vscode-editor-foreground)] hover:bg-[var(--vscode-list-hoverBackground)]`}
>
{title}
</button>
</Menu.Item>
<DropdownMenuItem
className={`${!isCurrent ? `font-normal` : `font-bold`} ${className || ''}`}
disabled={disabled}
onClick={(e) => onClick(value, e)}
>
{title}
</DropdownMenuItem>
);
};

View File

@@ -1,38 +0,0 @@
import { Menu, Transition } from '@headlessui/react';
import * as React from 'react';
import { Fragment } from 'react';
export interface IMenuItemsProps {
widthClass?: string;
marginTopClass?: string;
updatePopper?: () => void;
disablePopper?: boolean;
}
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({
widthClass,
marginTopClass,
children,
updatePopper,
disablePopper
}: React.PropsWithChildren<IMenuItemsProps>) => {
return (
<Transition
as={Fragment}
beforeEnter={() => (updatePopper ? updatePopper() : null)}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className={`${widthClass || ''} ${marginTopClass || 'mt-2'} ${disablePopper ? 'origin-top-right absolute right-0 z-20' : ''
} rounded shadow-2xl ring-1 ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto bg-[var(--vscode-sideBar-background)] ring-[var(--frontmatter-border)]`}
>
<div className="py-1">{children}</div>
</Menu.Items>
</Transition>
);
};

View File

@@ -1,12 +1,15 @@
import * as React from 'react';
import { cn } from '../../../utils/cn';
export interface IQuickActionProps {
title: string;
className?: string;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
}
export const QuickAction: React.FunctionComponent<IQuickActionProps> = ({
title,
className,
onClick,
children
}: React.PropsWithChildren<IQuickActionProps>) => {
@@ -15,7 +18,7 @@ export const QuickAction: React.FunctionComponent<IQuickActionProps> = ({
type="button"
title={title}
onClick={onClick}
className={`px-2 group inline-flex justify-center text-sm font-medium text-[var(--vscode-foreground)] hover:text-[var(--frontmatter-button-hoverBackground)]`}
className={cn(`px-2 group inline-flex justify-center text-sm font-medium text-[var(--frontmatter-text)] hover:text-[var(--frontmatter-button-hoverBackground)]`, className)}
>
{children}
<span className="sr-only">{title}</span>

View File

@@ -1,5 +1,4 @@
export * from './ActionMenuButton';
export * from './MenuButton';
export * from './MenuItem';
export * from './MenuItems';
export * from './QuickAction';

View File

@@ -0,0 +1,117 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { LocalizationKey } from '../../../localization';
import { GeneralCommands, ExtensionState } from '../../../constants';
import { SettingsInput } from './SettingsInput';
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react';
export interface IIntegrationsViewProps { }
export const IntegrationsView: React.FunctionComponent<IIntegrationsViewProps> = ({ }: React.PropsWithChildren<IIntegrationsViewProps>) => {
const [deeplApiKey, setDeeplApiKey] = React.useState<string>('');
const [azureApiKey, setAzureApiKey] = React.useState<string>('');
const [azureRegion, setAzureRegion] = React.useState<string>('');
const [crntDeeplApiKey, setCrntDeeplApiKey] = React.useState<string>('');
const [crntAzureApiKey, setCrntAzureApiKey] = React.useState<string>('');
const [crntAzureRegion, setCrntAzureRegion] = React.useState<string>('');
const onSave = React.useCallback(() => {
if (crntDeeplApiKey !== deeplApiKey) {
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
key: ExtensionState.Secrets.Deepl.ApiKey,
value: crntDeeplApiKey
}).then((apiKey: string) => {
setDeeplApiKey(apiKey);
});
}
if (crntAzureApiKey !== azureApiKey) {
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
key: ExtensionState.Secrets.Azure.TranslatorKey,
value: crntAzureApiKey
}).then((apiKey: string) => {
setAzureApiKey(apiKey);
});
}
if (crntAzureRegion !== azureRegion) {
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
key: ExtensionState.Secrets.Azure.TranslatorRegion,
value: crntAzureRegion
}).then((apiKey: string) => {
setAzureRegion(apiKey);
});
}
}, [crntDeeplApiKey, deeplApiKey, crntAzureApiKey, azureApiKey, crntAzureRegion, azureRegion]);
const onChange = (key: string, value: string) => {
if (key === ExtensionState.Secrets.Deepl.ApiKey) {
setCrntDeeplApiKey(value);
} else if (key === ExtensionState.Secrets.Azure.TranslatorKey) {
setCrntAzureApiKey(value);
} else if (key === ExtensionState.Secrets.Azure.TranslatorRegion) {
setCrntAzureRegion(value);
}
};
React.useEffect(() => {
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Deepl.ApiKey).then((apiKey: string) => {
setDeeplApiKey(apiKey);
setCrntDeeplApiKey(apiKey);
});
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Azure.TranslatorKey).then((apiKey: string) => {
setAzureApiKey(apiKey);
setCrntAzureApiKey(apiKey);
});
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Azure.TranslatorRegion).then((apiKey: string) => {
setAzureRegion(apiKey);
setCrntAzureRegion(apiKey);
});
}, []);
return (
<div className='w-full divide-y divide-[var(--frontmatter-border)]'>
<div className='py-4 space-y-4'>
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsIntegrationsViewDeeplTitle)}</h2>
<SettingsInput
label={l10n.t(LocalizationKey.settingsIntegrationsViewDeeplIntputLabel)}
name={ExtensionState.Secrets.Deepl.ApiKey}
value={crntDeeplApiKey || ""}
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewDeeplIntputPlaceholder)}
onChange={onChange}
/>
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsIntegrationsViewAzureTitle)}</h2>
<SettingsInput
label={l10n.t(LocalizationKey.settingsIntegrationsViewAzureIntputLabel)}
name={ExtensionState.Secrets.Azure.TranslatorKey}
value={crntAzureApiKey || ""}
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewAzureIntputPlaceholder)}
onChange={onChange}
/>
<SettingsInput
label={l10n.t(LocalizationKey.settingsIntegrationsViewAzureRegionLabel)}
name={ExtensionState.Secrets.Azure.TranslatorRegion}
value={crntAzureRegion || ""}
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewAzureRegionPlaceholder)}
onChange={onChange}
/>
<div className={`mt-4 flex gap-2`}>
<VSCodeButton
onClick={onSave}
disabled={
deeplApiKey === crntDeeplApiKey &&
azureApiKey === crntAzureApiKey &&
azureRegion === crntAzureRegion
}>
{l10n.t(LocalizationKey.commonSave)}
</VSCodeButton>
</div>
</div>
</div>
);
};

View File

@@ -3,6 +3,8 @@ import { useRecoilValue } from 'recoil';
import { SettingsSelector } from '../../state';
import { CogIcon } from '@heroicons/react/24/solid';
import { NavigationType } from '../../models';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
export interface ISettingsLinkProps {
onNavigate: (navigationType: NavigationType) => void;
@@ -20,11 +22,11 @@ export const SettingsLink: React.FunctionComponent<ISettingsLinkProps> = ({
return (
<button
className="flex items-center mr-4 hover:text-[var(--vscode-textLink-activeForeground)]"
title={`Settings`}
title={l10n.t(LocalizationKey.commonSettings)}
onClick={() => onNavigate(NavigationType.Settings)}
>
<CogIcon className="h-4 w-4" />
<span className='sr-only'>Settings</span>
<span className='sr-only'>{l10n.t(LocalizationKey.commonSettings)}</span>
</button>
);
};

View File

@@ -12,6 +12,7 @@ import { COMMAND_NAME } from '../../../constants';
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { VSCodePanelTab, VSCodePanelView, VSCodePanels } from '@vscode/webview-ui-toolkit/react';
import { CommonSettings } from './CommonSettings';
import { IntegrationsView } from './IntegrationsView';
export interface ISettingsViewProps { }
@@ -54,6 +55,8 @@ export const SettingsView: React.FunctionComponent<ISettingsViewProps> = (_: Rea
)
}
<VSCodePanelTab id="view-4">{l10n.t(LocalizationKey.settingsViewIntegration)}</VSCodePanelTab>
<VSCodePanelView id="view-1">
<CommonSettings />
</VSCodePanelView>
@@ -82,6 +85,10 @@ export const SettingsView: React.FunctionComponent<ISettingsViewProps> = (_: Rea
</VSCodePanelView>
)
}
<VSCodePanelView id="view-4">
<IntegrationsView />
</VSCodePanelView>
</VSCodePanels>
</div>
)

View File

@@ -25,6 +25,7 @@ import { NewForm } from './NewForm';
import SnippetForm, { SnippetFormHandle } from './SnippetForm';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { openFile } from '../../utils';
export interface IItemProps {
snippetKey: string;
@@ -65,7 +66,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
};
const showFile = useCallback(() => {
Messenger.send(DashboardMessage.openFile, snippet.sourcePath);
openFile(snippet.sourcePath);
}, [snippet]);
const onOpenEdit = useCallback(() => {
@@ -163,7 +164,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
<>
<li className={`group relative overflow-hidden shadow-md hover:shadow-xl dark:shadow-none border p-4 space-y-2 rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] border-[var(--frontmatter-border)]`}>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<CodeBracketIcon className={`w-64 h-64 opacity-5 text-[var(--vscode-foreground)]`} />
<CodeBracketIcon className={`w-64 h-64 opacity-5 text-[var(--frontmatter-text)]`} />
</div>
<h2
@@ -243,7 +244,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
</div>
</FeatureFlag>
<p className={`text-xs text-[var(--vscode-foreground)]`}>{snippet.description}</p>
<p className={`text-xs text-[var(--frontmatter-text)]`}>{snippet.description}</p>
</li>
{showInsertDialog && (
@@ -260,6 +261,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
ref={formRef}
snippetKey={snippetKey}
snippet={snippet}
filePath={viewData?.data?.filePath}
fieldInfo={viewData?.data?.snippetInfo?.fields}
selection={viewData?.data?.selection} />
</FormDialog>

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