mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
661efcf23f | ||
|
|
152f36e352 | ||
|
|
08697abba4 | ||
|
|
f1d345ebc2 | ||
|
|
e9af7e1793 | ||
|
|
49e7fe6377 | ||
|
|
cc375801c2 | ||
|
|
4a53a180a7 | ||
|
|
51ece235f8 | ||
|
|
36ac891c00 | ||
|
|
5f0fd29cca | ||
|
|
0428e561a8 | ||
|
|
bcba947c1d | ||
|
|
c2d3496152 | ||
|
|
7f1dc88bd4 | ||
|
|
83f4711103 | ||
|
|
0a8723c544 | ||
|
|
bdce486a24 | ||
|
|
6d6a53047a | ||
|
|
afb241ad6a | ||
|
|
4229d262ae | ||
|
|
6b92a6f8b4 | ||
|
|
183e77b77b | ||
|
|
da7d5e6854 | ||
|
|
8a08f54340 | ||
|
|
be54b6286f | ||
|
|
1315602bcc | ||
|
|
0ad0179a4b | ||
|
|
9d68797c95 | ||
|
|
ffaea3b55d | ||
|
|
4565ea75ae | ||
|
|
c4d3f76510 | ||
|
|
ce2bd06f6d | ||
|
|
a29a6600ab | ||
|
|
6cbf86f822 | ||
|
|
514272835a | ||
|
|
3c29df54c1 | ||
|
|
d06be0efa1 | ||
|
|
2375be9211 | ||
|
|
06b8a579a8 | ||
|
|
460c4964f6 | ||
|
|
7c4aa1d63d | ||
|
|
6e84217458 | ||
|
|
b1380388b6 | ||
|
|
d70f983694 | ||
|
|
d22ebfa6ce | ||
|
|
cf96923d96 | ||
|
|
6150a34547 | ||
|
|
d45cd0d015 |
4
.github/workflows/release-beta.yml
vendored
4
.github/workflows/release-beta.yml
vendored
@@ -13,8 +13,8 @@ jobs:
|
||||
name: Beta
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -13,8 +13,8 @@ jobs:
|
||||
name: Stable
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
- [#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
|
||||
- [#752](https://github.com/estruyf/vscode-front-matter/issues/752): Placeholder support in default `list` field values
|
||||
|
||||
### ⚡️ Optimizations
|
||||
|
||||
|
||||
3
assets/icons/i18n-dark.svg
Normal file
3
assets/icons/i18n-dark.svg
Normal 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 |
3
assets/icons/i18n-light.svg
Normal file
3
assets/icons/i18n-light.svg
Normal 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 |
@@ -35,6 +35,8 @@
|
||||
"common.no": "no",
|
||||
"common.openSettings": "Open settings",
|
||||
"common.back": "Back",
|
||||
"common.open": "Open",
|
||||
"common.openWithValue": "Open: {0}",
|
||||
|
||||
"loading.initPages": "Loading content",
|
||||
|
||||
@@ -44,6 +46,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 +59,17 @@
|
||||
"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": "Authentication key",
|
||||
"settings.integrationsView.deepl.intput.placeholder": "Enter your DeepL authentication key",
|
||||
|
||||
"developer.title": "Developer mode",
|
||||
"developer.reload.title": "Reload the dashboard",
|
||||
"developer.reload.label": "Reload",
|
||||
@@ -88,6 +97,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 +137,9 @@
|
||||
|
||||
"dashboard.errorView.description": "Please close the dashboard and try again.",
|
||||
|
||||
"dashboard.filters.languageFilter.label": "Locale",
|
||||
"dashboard.filters.languageFilter.all": "All",
|
||||
|
||||
"dashboard.header.breadcrumb.home": "Home",
|
||||
|
||||
"dashboard.header.clearFilters.title": "Clear filters, grouping, and sorting",
|
||||
@@ -209,10 +223,14 @@
|
||||
"dashboard.media.folderCreation.hexo.create": "Create post asset folder",
|
||||
"dashboard.media.folderCreation.folder.create": "Create new folder",
|
||||
|
||||
"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 +311,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 +358,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",
|
||||
@@ -511,6 +535,17 @@
|
||||
"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.warning.notDefaultLocale": "The current file cannot be used for i18n content creation.",
|
||||
"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",
|
||||
|
||||
@@ -700,6 +735,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",
|
||||
|
||||
787
package-lock.json
generated
787
package-lock.json
generated
@@ -8,15 +8,17 @@
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "9.5.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
"@bendera/vscode-webview-elements": "0.6.2",
|
||||
"@estruyf/vscode": "^1.1.0",
|
||||
"@headlessui/react": "^1.7.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",
|
||||
@@ -44,6 +46,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",
|
||||
@@ -79,7 +82,6 @@
|
||||
"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",
|
||||
@@ -89,7 +91,9 @@
|
||||
"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",
|
||||
@@ -389,7 +393,6 @@
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
|
||||
"integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@@ -539,6 +542,40 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz",
|
||||
"integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.0.0",
|
||||
"@floating-ui/utils": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz",
|
||||
"integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
|
||||
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "1.7.18",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",
|
||||
@@ -960,6 +997,535 @@
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
|
||||
"integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",
|
||||
"integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-primitive": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
|
||||
"integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-slot": "1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
|
||||
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
|
||||
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz",
|
||||
"integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
|
||||
"integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz",
|
||||
"integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-id": "1.0.1",
|
||||
"@radix-ui/react-menu": "2.0.6",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
|
||||
"integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
|
||||
"integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
|
||||
"integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz",
|
||||
"integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-collection": "1.0.3",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-direction": "1.0.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.0.5",
|
||||
"@radix-ui/react-focus-guards": "1.0.1",
|
||||
"@radix-ui/react-focus-scope": "1.0.4",
|
||||
"@radix-ui/react-id": "1.0.1",
|
||||
"@radix-ui/react-popper": "1.1.3",
|
||||
"@radix-ui/react-portal": "1.0.4",
|
||||
"@radix-ui/react-presence": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-roving-focus": "1.0.4",
|
||||
"@radix-ui/react-slot": "1.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
|
||||
"integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.0.3",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1",
|
||||
"@radix-ui/react-use-rect": "1.0.1",
|
||||
"@radix-ui/react-use-size": "1.0.1",
|
||||
"@radix-ui/rect": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
|
||||
"integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-primitive": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
|
||||
"integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
|
||||
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz",
|
||||
"integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-collection": "1.0.3",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-direction": "1.0.1",
|
||||
"@radix-ui/react-id": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
||||
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
|
||||
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
|
||||
"integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz",
|
||||
"integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
|
||||
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-rect": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz",
|
||||
"integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/rect": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz",
|
||||
"integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/rect": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz",
|
||||
"integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/portal": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz",
|
||||
@@ -1390,7 +1956,7 @@
|
||||
"version": "15.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.11",
|
||||
@@ -1417,7 +1983,7 @@
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz",
|
||||
"integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -1439,7 +2005,7 @@
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz",
|
||||
"integrity": "sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
@@ -2216,6 +2782,22 @@
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
|
||||
"integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-hidden/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/array-buffer-byte-length": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
|
||||
@@ -2841,6 +3423,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
|
||||
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -3120,7 +3711,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.23.0",
|
||||
@@ -3296,6 +3887,11 @@
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/detect-node-es": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@@ -4442,6 +5038,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -5284,7 +5888,6 @@
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
@@ -5822,8 +6425,7 @@
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
@@ -6103,7 +6705,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
@@ -7505,7 +8106,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9231,7 +9831,6 @@
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz",
|
||||
"integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
@@ -9262,7 +9861,6 @@
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz",
|
||||
"integrity": "sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -9382,6 +9980,61 @@
|
||||
"react-dom": "^16 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
|
||||
"integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.3",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll-bar": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
|
||||
"integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
|
||||
"dependencies": {
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll-bar/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/react-remove-scroll/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz",
|
||||
@@ -9430,6 +10083,33 @@
|
||||
"react-dom": "^16.3.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
||||
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
|
||||
"dependencies": {
|
||||
"get-nonce": "^1.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -9535,8 +10215,7 @@
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.1",
|
||||
@@ -9941,7 +10620,6 @@
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
|
||||
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
@@ -10803,6 +11481,19 @@
|
||||
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.1.tgz",
|
||||
"integrity": "sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
|
||||
@@ -10840,6 +11531,15 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
|
||||
"integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || insiders"
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
|
||||
@@ -11504,6 +12204,57 @@
|
||||
"integrity": "sha512-u+5gi7JyOwhj58ZKwkmkzFGHuepTpmwjqfUDGVjsJJstsCz63CJAINixgJaDcMbmuyWPJIxbtBpIfaDgOQ9KMQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/use-callback-ref": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz",
|
||||
"integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-callback-ref/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
||||
"integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
|
||||
"dependencies": {
|
||||
"detect-node-es": "^1.1.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||
|
||||
199
package.json
199
package.json
@@ -303,6 +303,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,
|
||||
@@ -313,6 +324,34 @@
|
||||
},
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.placeholders": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
@@ -492,8 +531,7 @@
|
||||
"categories"
|
||||
],
|
||||
"markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%",
|
||||
"items": [
|
||||
{
|
||||
"items": [{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
@@ -888,6 +926,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%",
|
||||
@@ -996,6 +1050,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": [
|
||||
@@ -2191,6 +2318,15 @@
|
||||
"command": "frontMatter.cache.clear",
|
||||
"title": "%command.frontMatter.cache.clear%",
|
||||
"category": "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": [{
|
||||
@@ -2232,6 +2368,11 @@
|
||||
"group": "navigation@-128",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.i18n.create",
|
||||
"group": "navigation@-127",
|
||||
"when": "frontMatter:file:isValid && frontMatter:i18n:default"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.options",
|
||||
"group": "navigation@-126",
|
||||
@@ -2311,14 +2452,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"
|
||||
@@ -2331,10 +2464,26 @@
|
||||
"command": "frontMatter.git.sync",
|
||||
"when": "frontMatter:git:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.i18n.create",
|
||||
"when": "frontMatter:i18n:default"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.authenticate",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertTags",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertCategories",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.registerFolder",
|
||||
"when": "false"
|
||||
@@ -2395,10 +2544,22 @@
|
||||
"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.insertSnippet",
|
||||
"when": "frontMatter:file:isValid == true && frontMatter:dashboard:snippets:enabled"
|
||||
@@ -2419,14 +2580,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"
|
||||
@@ -2557,11 +2710,10 @@
|
||||
"@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",
|
||||
@@ -2589,6 +2741,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",
|
||||
@@ -2624,7 +2777,6 @@
|
||||
"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",
|
||||
@@ -2634,7 +2786,9 @@
|
||||
"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",
|
||||
@@ -2653,5 +2807,8 @@
|
||||
},
|
||||
"vsce": {
|
||||
"dependencies": false
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6"
|
||||
}
|
||||
}
|
||||
@@ -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)",
|
||||
@@ -253,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)"
|
||||
}
|
||||
@@ -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';
|
||||
@@ -32,6 +34,7 @@ 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;
|
||||
@@ -52,6 +55,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
|
||||
*/
|
||||
|
||||
@@ -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,7 +10,7 @@ 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';
|
||||
@@ -288,67 +289,34 @@ export class Folders {
|
||||
const folderInfo: FolderInfo[] = [];
|
||||
|
||||
for (const folder of folders) {
|
||||
try {
|
||||
const folderPath = parseWinPath(folder.path);
|
||||
const crntFolderInfo = await Folders.getFilesByFolder(folder, supportedFiles, limit);
|
||||
if (crntFolderInfo) {
|
||||
folderInfo.push(crntFolderInfo);
|
||||
}
|
||||
|
||||
if (typeof folderPath === 'string') {
|
||||
let files: Uri[] = [];
|
||||
// Process localization folders
|
||||
if (folder.defaultLocale) {
|
||||
const i18nConfig = folder.locales || Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
|
||||
if (i18nConfig) {
|
||||
for (const i18n of i18nConfig) {
|
||||
if (i18n.locale !== folder.defaultLocale && i18n.path) {
|
||||
const i18nFolder = {
|
||||
...folder,
|
||||
path: join(folder.path, i18n.path),
|
||||
title: `${folder.title} (${i18n.title})`
|
||||
} as ContentFolder;
|
||||
|
||||
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
|
||||
const crntFolderInfo = await Folders.getFilesByFolder(
|
||||
i18nFolder,
|
||||
supportedFiles,
|
||||
limit
|
||||
);
|
||||
if (crntFolderInfo) {
|
||||
folderInfo.push(crntFolderInfo);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,6 +571,97 @@ 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
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip the current folder
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all content folders
|
||||
* @param pattern
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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 isI18nDefault = await i18n.isDefaultLanguage(document.uri.fsPath);
|
||||
commands.executeCommand('setContext', CONTEXT.isI18nDefault, isI18nDefault);
|
||||
|
||||
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.isI18nDefault, false);
|
||||
|
||||
const panel = PanelProvider.getInstance();
|
||||
if (panel && panel.visible) {
|
||||
|
||||
517
src/commands/i18n.ts
Normal file
517
src/commands/i18n.ts
Normal file
@@ -0,0 +1,517 @@
|
||||
import { ProgressLocation, Uri, commands, window, workspace } from 'vscode';
|
||||
import {
|
||||
ArticleHelper,
|
||||
ContentType,
|
||||
Extension,
|
||||
FrameworkDetector,
|
||||
Logger,
|
||||
Notifications,
|
||||
Settings,
|
||||
openFileInEditor,
|
||||
parseWinPath
|
||||
} from '../helpers';
|
||||
import { COMMAND_NAME, ExtensionState, 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';
|
||||
|
||||
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 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 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) {
|
||||
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 &&
|
||||
parseWinPath(fileInfo.dir).toLowerCase() === parseWinPath(pageFolderPath).toLowerCase()
|
||||
) {
|
||||
return i18nSettings.find((i18n) => i18n.locale === pageFolder?.defaultLocale);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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 i18nSettings = await i18n.getSettings(fileUri.fsPath);
|
||||
if (!i18nSettings) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
|
||||
return;
|
||||
}
|
||||
|
||||
const isDefaultLanguage = await i18n.isDefaultLanguage(fileUri.fsPath);
|
||||
if (!isDefaultLanguage) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNotDefaultLocale));
|
||||
return;
|
||||
}
|
||||
|
||||
const locale = await window.showQuickPick(
|
||||
i18nSettings.filter((i18n) => i18n.path).map((i18n) => i18n.title || i18n.locale),
|
||||
{
|
||||
title: l10n.t(LocalizationKey.commandsI18nCreateQuickPickTitle),
|
||||
placeHolder: l10n.t(LocalizationKey.commandsI18nCreateQuickPickPlaceHolder),
|
||||
ignoreFocusOut: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!locale) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedI18n = i18nSettings.find(
|
||||
(i18n) => i18n.title === locale || i18n.locale === locale
|
||||
);
|
||||
if (!selectedI18n || !selectedI18n.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(dir, selectedI18n.path, pageBundleDir);
|
||||
|
||||
if (!(await existsAsync(i18nDir))) {
|
||||
await workspace.fs.createDirectory(Uri.file(i18nDir));
|
||||
}
|
||||
|
||||
article = await i18n.updateFrontMatter(
|
||||
article,
|
||||
fileUri.fsPath,
|
||||
contentType,
|
||||
selectedI18n,
|
||||
i18nDir
|
||||
);
|
||||
|
||||
const newFilePath = join(i18nDir, fileInfo.base);
|
||||
if (await existsAsync(newFilePath)) {
|
||||
Notifications.error(l10n.t(LocalizationKey.commandsI18nCreateErrorFileExists));
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceLocale = await i18n.getLocale(fileUri.fsPath);
|
||||
if (sourceLocale?.locale) {
|
||||
article = await i18n.translate(article, sourceLocale, selectedI18n);
|
||||
}
|
||||
|
||||
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,
|
||||
selectedI18n.title || selectedI18n.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) => {
|
||||
const authKey = await Extension.getInstance().getSecret(ExtensionState.Secrets.DeeplApiKey);
|
||||
if (!authKey) {
|
||||
resolve(article);
|
||||
return;
|
||||
}
|
||||
|
||||
await window.withProgress(
|
||||
{
|
||||
location: ProgressLocation.Notification,
|
||||
title: l10n.t(LocalizationKey.commandsI18nTranslateProgressTitle),
|
||||
cancellable: false
|
||||
},
|
||||
async () => {
|
||||
const title = article.data.title;
|
||||
const description = article.data.description;
|
||||
const content = article.content;
|
||||
|
||||
try {
|
||||
const body = JSON.stringify({
|
||||
text: [title, description, content],
|
||||
source_lang: sourceLocale.locale,
|
||||
target_lang: targetLocale.locale
|
||||
});
|
||||
|
||||
let host = authKey.endsWith(':fx') ? 'api-free.deepl.com' : 'api.deepl.com';
|
||||
|
||||
const response = await fetch(`https://${host}/v2/translate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `DeepL-Auth-Key ${authKey}`,
|
||||
'User-Agent': `FrontMatterCMS/${Extension.getInstance().version}`,
|
||||
'Content-Type': 'application/json',
|
||||
'content-length': body.length.toString(),
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`DeepL: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.translations || data.translations.length < 3) {
|
||||
throw new Error('DeepL: Invalid response');
|
||||
}
|
||||
|
||||
article.data.title = data.translations[0].text;
|
||||
article.data.description = data.translations[1].text;
|
||||
article.content = data.translations[2].text;
|
||||
} 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 i18nConfig - The configuration for internationalization.
|
||||
* @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,
|
||||
i18nConfig: 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;
|
||||
}
|
||||
}
|
||||
149
src/components/shadcn/Dropdown.tsx
Normal file
149
src/components/shadcn/Dropdown.tsx
Normal 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,
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
|
||||
@@ -30,5 +30,9 @@ export const ExtensionState = {
|
||||
v7_0_0: {
|
||||
dateFields: `frontMatter:Updates:v7.0.0:dateFields`
|
||||
}
|
||||
},
|
||||
|
||||
Secrets: {
|
||||
DeeplApiKey: `frontMatter:Secrets:DeeplApiKey`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
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'
|
||||
},
|
||||
runCommand: 'runCommand',
|
||||
getLocalization: 'getLocalization',
|
||||
openOnWebsite: 'openOnWebsite'
|
||||
}
|
||||
|
||||
@@ -49,5 +49,6 @@ export const TelemetryEvent = {
|
||||
webviewTaxonomyDashboard: 'webviewTaxonomyDashboard',
|
||||
|
||||
// Git
|
||||
gitSync: 'gitSync'
|
||||
gitSync: 'gitSync',
|
||||
gitFetch: 'gitFetch'
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@ export const CONTEXT = {
|
||||
isValidFile: 'frontMatter:file:isValid',
|
||||
isDevelopment: 'frontMatter:isDevelopment',
|
||||
|
||||
isI18nDefault: 'frontMatter:i18n:default',
|
||||
|
||||
hasViewModes: 'frontMatter:has:modes',
|
||||
|
||||
isSnippetsDashboardEnabled: 'frontMatter:dashboard:snippets:enabled',
|
||||
|
||||
@@ -57,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';
|
||||
@@ -76,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';
|
||||
@@ -100,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';
|
||||
|
||||
@@ -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 >
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { Messenger, messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { EyeIcon, GlobeEuropeAfricaIcon, CommandLineIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { EyeIcon, GlobeEuropeAfricaIcon, CommandLineIcon, TrashIcon, EllipsisVerticalIcon, LanguageIcon } from '@heroicons/react/24/outline';
|
||||
import * as React from 'react';
|
||||
import { CustomScript, ScriptType } from '../../../models';
|
||||
import { CustomScript, I18nConfig, ScriptType } from '../../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { MenuItem, MenuItems, ActionMenuButton, QuickAction } from '../Menu';
|
||||
import { 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 { COMMAND_NAME, GeneralCommands } from '../../../constants';
|
||||
import { PinIcon } from '../Icons/PinIcon';
|
||||
import { PinnedItemsAtom } from '../../state/atom/PinnedItems';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
|
||||
|
||||
export interface IContentActionsProps {
|
||||
title: string;
|
||||
@@ -22,6 +20,14 @@ export interface IContentActionsProps {
|
||||
relPath: string;
|
||||
scripts: CustomScript[] | undefined;
|
||||
listView?: boolean;
|
||||
locale?: I18nConfig;
|
||||
isDefaultLocale?: boolean;
|
||||
translations?: {
|
||||
[locale: string]: {
|
||||
locale: I18nConfig;
|
||||
path: string;
|
||||
};
|
||||
};
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
@@ -31,25 +37,21 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
relPath,
|
||||
scripts,
|
||||
onOpen,
|
||||
listView
|
||||
listView,
|
||||
isDefaultLocale,
|
||||
translations,
|
||||
locale
|
||||
}: React.PropsWithChildren<IContentActionsProps>) => {
|
||||
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 = (e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
setShowDeletionAlert(true);
|
||||
};
|
||||
@@ -61,7 +63,11 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
setShowDeletionAlert(false);
|
||||
};
|
||||
|
||||
const openOnWebsite = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const onOpenFile = (filePath: string) => {
|
||||
messageHandler.send(DashboardMessage.openFile, filePath);
|
||||
}
|
||||
|
||||
const openOnWebsite = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
if (settings?.websiteUrl && path) {
|
||||
Messenger.send(GeneralCommands.toVSCode.openOnWebsite, {
|
||||
@@ -71,14 +77,14 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
}
|
||||
}, [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 || []);
|
||||
@@ -86,13 +92,20 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
}, [path]);
|
||||
|
||||
const runCustomScript = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>, script: CustomScript) => {
|
||||
(e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>, 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]);
|
||||
@@ -106,19 +119,56 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
!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)}
|
||||
/>
|
||||
<DropdownMenuItem key={script.id || script.title} onClick={(e) => runCustomScript(e, script)}>
|
||||
<CommandLineIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
|
||||
<span>{script.title}</span>
|
||||
</DropdownMenuItem>
|
||||
));
|
||||
}, [scripts]);
|
||||
|
||||
const translationsMenu = React.useMemo(() => {
|
||||
if (!locale || !translations || Object.keys(translations).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const crntLocale = translations[locale.locale];
|
||||
const otherLocales = Object.entries(translations).filter(([key]) => key !== locale.locale);
|
||||
|
||||
if (otherLocales.length === 0) {
|
||||
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={() => onOpenFile(crntLocale.path)}>
|
||||
<span>{crntLocale.locale.title || crntLocale.locale.locale}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{
|
||||
otherLocales.map(([key, value]) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
onClick={() => onOpenFile(value.path)}
|
||||
>
|
||||
<span>{value.locale.title || value.locale.locale}</span>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
);
|
||||
}, [translations, locale, isDefaultLocale]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -129,7 +179,7 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
className={`flex items-center border border-transparent rounded-full ${listView ? '' : 'p-2 -mt-4'
|
||||
} group-hover/card:bg-[var(--vscode-sideBar-background)] group-hover/card:border-[var(--frontmatter-border)]`}
|
||||
>
|
||||
<Menu as="div" className={`relative flex text-left`}>
|
||||
<div className={`relative flex text-left`}>
|
||||
{!listView && (
|
||||
<div className="hidden group-hover/card:flex">
|
||||
<QuickAction title={l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)} onClick={onView}>
|
||||
@@ -144,74 +194,62 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
<QuickAction title={l10n.t(LocalizationKey.commonDelete)} onClick={onDelete}>
|
||||
<QuickAction
|
||||
title={l10n.t(LocalizationKey.commonDelete)}
|
||||
className={`hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
|
||||
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>
|
||||
<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>
|
||||
<EllipsisVerticalIcon className="w-4 h-4" aria-hidden="true" />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<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)}
|
||||
/>
|
||||
<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>
|
||||
|
||||
{
|
||||
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)}
|
||||
/>
|
||||
<DropdownMenuItem onClick={openOnWebsite}>
|
||||
<GlobeEuropeAfricaIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
|
||||
<span>{l10n.t(LocalizationKey.commonOpenOnWebsite)}</span>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
locale && isDefaultLocale && (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
{translationsMenu}
|
||||
|
||||
{customScriptActions}
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
||||
26
src/dashboardWebView/components/Contents/I18nLabel.tsx
Normal file
26
src/dashboardWebView/components/Contents/I18nLabel.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import { LocalizationKey } from '../../../localization';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { routePaths } from '../..';
|
||||
import useCard from '../../hooks/useCard';
|
||||
import { I18nLabel } from './I18nLabel';
|
||||
|
||||
export interface IItemProps extends Page { }
|
||||
|
||||
@@ -69,6 +70,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-4' dangerouslySetInnerHTML={{ __html: dateHtml }} />
|
||||
) : (
|
||||
cardFields?.date && pageData.date ? <DateField className={`mr-4`} value={pageData.date} format={pageData.fmDateFormat} /> : null
|
||||
)
|
||||
)
|
||||
}, [dateHtml, cardFields?.date, pageData]);
|
||||
|
||||
const hasDraftOrDate = useMemo(() => {
|
||||
return cardFields && (cardFields.state || cardFields.date);
|
||||
}, [cardFields]);
|
||||
@@ -80,9 +109,9 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
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)]`}
|
||||
>
|
||||
<button
|
||||
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
|
||||
onClick={openFile}
|
||||
className={`relative h-36 w-full overflow-hidden border-b cursor-pointer border-[var(--frontmatter-border)]
|
||||
}`}
|
||||
className={`relative h-36 w-full overflow-hidden border-b cursor-pointer border-[var(--frontmatter-border)]`}
|
||||
>
|
||||
{
|
||||
imageHtml ?
|
||||
@@ -105,53 +134,59 @@ 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} />
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
dateHtml ? (
|
||||
<div className='mr-4' dangerouslySetInnerHTML={{ __html: dateHtml }} />
|
||||
) : (
|
||||
cardFields?.date && <DateField className={`mr-4`} value={pageData.date} format={pageData.fmDateFormat} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
(statusPlaceholder || datePlaceholder) && (
|
||||
<div className={`flex justify-between items-center ${hasDraftOrDate ? `mb-2` : ``}`}>
|
||||
{statusPlaceholder}
|
||||
{datePlaceholder}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<ContentActions
|
||||
title={pageData.title}
|
||||
path={pageData.fmFilePath}
|
||||
relPath={pageData.fmRelFileWsPath}
|
||||
locale={pageData.fmLocale}
|
||||
isDefaultLocale={pageData.fmDefaultLocale}
|
||||
translations={pageData.fmTranslations}
|
||||
scripts={settings?.scripts}
|
||||
onOpen={openFile}
|
||||
/>
|
||||
|
||||
<button onClick={openFile} className={`text-left block`}>
|
||||
<I18nLabel page={pageData} />
|
||||
|
||||
<button
|
||||
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
|
||||
onClick={openFile}
|
||||
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={openFile}
|
||||
className={`mt-2 text-left block`}>
|
||||
{
|
||||
descriptionHtml ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
|
||||
) : (
|
||||
<p className={`text-xs text-[vara(--vscode-titleBar-activeForeground)]`}>{escapedDescription}</p>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
tagsHtml ? (
|
||||
@@ -197,7 +232,9 @@ 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}>
|
||||
<button
|
||||
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
|
||||
onClick={openFile}>
|
||||
{escapedTitle}
|
||||
</button>
|
||||
|
||||
|
||||
66
src/dashboardWebView/components/Filters/LanguageFilter.tsx
Normal file
66
src/dashboardWebView/components/Filters/LanguageFilter.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,7 +18,6 @@ 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 otherFilterValues = useMemo(() => {
|
||||
@@ -74,6 +74,8 @@ export const Filters: React.FunctionComponent<IFiltersProps> = (_: React.PropsWi
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageFilter />
|
||||
|
||||
{
|
||||
settings?.filters?.includes("pageFolders") && (
|
||||
<FoldersFilter />
|
||||
|
||||
@@ -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, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
|
||||
|
||||
export interface IFoldersFilterProps { }
|
||||
|
||||
@@ -21,29 +21,27 @@ export const FoldersFilter: React.FunctionComponent<
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -164,7 +164,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
|
||||
<Searchbox />
|
||||
|
||||
<div className={`flex items-center justify-end space-x-4 flex-1`}>
|
||||
<SyncButton />
|
||||
{/* <SyncButton /> */}
|
||||
|
||||
<ChoiceButton
|
||||
title={l10n.t(LocalizationKey.dashboardHeaderHeaderCreateContent)}
|
||||
|
||||
@@ -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 >
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
227
src/dashboardWebView/components/Media/DetailsForm.tsx
Normal file
227
src/dashboardWebView/components/Media/DetailsForm.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 title={l10n.t(LocalizationKey.dashboardMediaCommonTitle)} details={media.metadata.title || ""} />
|
||||
);
|
||||
} else if (field.name === "caption") {
|
||||
if (isImageFile) {
|
||||
items.push(
|
||||
<DetailsItem title={l10n.t(LocalizationKey.dashboardMediaCommonCaption)} details={media.metadata.caption || ""} />
|
||||
);
|
||||
}
|
||||
} else if (field.name === "alt") {
|
||||
if (isImageFile) {
|
||||
items.push(
|
||||
<DetailsItem title={l10n.t(LocalizationKey.dashboardMediaCommonAlt)} details={media.metadata.alt || ""} />
|
||||
);
|
||||
}
|
||||
} else {
|
||||
items.push(
|
||||
<DetailsItem title={field.title || field.name} details={(media.metadata[field.name] || "") as string} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [fields, media.metadata]);
|
||||
|
||||
return (
|
||||
<Transition.Root show={true} as={Fragment}>
|
||||
@@ -168,101 +160,11 @@ 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 && (
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
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 * 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 {
|
||||
@@ -29,23 +21,22 @@ import {
|
||||
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';
|
||||
|
||||
export interface IItemProps {
|
||||
media: MediaInfo;
|
||||
}
|
||||
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
media
|
||||
media,
|
||||
}: React.PropsWithChildren<IItemProps>) => {
|
||||
const [, setLightbox] = useRecoilState(LightboxAtom);
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
@@ -55,24 +46,19 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
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 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 [];
|
||||
@@ -101,44 +87,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
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 +103,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 +116,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 +139,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 +164,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
setMediaData(fieldData);
|
||||
}
|
||||
},
|
||||
[alt, caption, media, settings, viewData, mediaSnippets]
|
||||
[media, settings, viewData, mediaSnippets, relPath]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -223,8 +172,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 +180,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,
|
||||
@@ -290,10 +226,6 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
return sizeDetails.join(' - ');
|
||||
};
|
||||
|
||||
const viewMediaDetails = () => {
|
||||
setShowDetails(true);
|
||||
};
|
||||
|
||||
const openLightbox = useCallback(() => {
|
||||
if (isImageFile) {
|
||||
setLightbox(media.vsPath || '');
|
||||
@@ -305,23 +237,6 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
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;
|
||||
@@ -404,18 +319,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) {
|
||||
@@ -455,9 +358,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 +368,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}
|
||||
>
|
||||
@@ -480,171 +383,45 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
)}
|
||||
</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>
|
||||
<ItemMenu
|
||||
media={media}
|
||||
relPath={relPath}
|
||||
selectedFolder={selectedFolder}
|
||||
viewData={viewData?.data}
|
||||
snippets={mediaSnippets}
|
||||
scripts={settings?.scripts}
|
||||
insertIntoArticle={insertIntoArticle}
|
||||
insertSnippet={insertSnippet}
|
||||
showUpdateMedia={updateMetadata}
|
||||
showMediaDetails={() => setShowDetails(true)}
|
||||
processSnippet={processSnippet}
|
||||
onDelete={() => setShowAlert(true)} />
|
||||
|
||||
<QuickAction title="Edit metadata" onClick={updateMetadata}>
|
||||
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
|
||||
{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)]}`}>
|
||||
{basename(parseWinPath(media.fsPath) || '')}
|
||||
</p>
|
||||
{!isImageFile && media.title && (
|
||||
{!isImageFile && media.metadata.title && (
|
||||
<p className={`mt-2 text-xs font-medium pointer-events-none flex flex-col items-start`}>
|
||||
<b className={`mr-2`}>
|
||||
{l10n.t(LocalizationKey.dashboardMediaCommonTitle)}:
|
||||
</b>
|
||||
<span className={`block mt-1 text-xs text-[var(--vscode-foreground)]`}>{media.title}</span>
|
||||
<span className={`block mt-1 text-xs text-[var(--vscode-foreground)]`}>{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`}>
|
||||
{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(--vscode-foreground)]`}>{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`}>
|
||||
{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(--vscode-foreground)]`}>{media.metadata.alt}</span>
|
||||
</p>
|
||||
)}
|
||||
{(media?.size || media?.dimensions) && (
|
||||
|
||||
193
src/dashboardWebView/components/Media/ItemMenu.tsx
Normal file
193
src/dashboardWebView/components/Media/ItemMenu.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import * as React from 'react';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { QuickAction } from '../Menu';
|
||||
import { ClipboardIcon, CodeBracketIcon, CommandLineIcon, EllipsisVerticalIcon, EyeIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { CustomScript, MediaInfo, ScriptType, 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';
|
||||
|
||||
export interface IItemMenuProps {
|
||||
media: MediaInfo;
|
||||
relPath?: string;
|
||||
selectedFolder: string | null;
|
||||
viewData?: ViewData;
|
||||
snippets: Snippet[];
|
||||
scripts?: CustomScript[];
|
||||
insertIntoArticle: () => void;
|
||||
insertSnippet: () => void;
|
||||
showUpdateMedia: () => void;
|
||||
showMediaDetails: () => void;
|
||||
processSnippet: (snippet: Snippet) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
|
||||
media,
|
||||
relPath,
|
||||
selectedFolder,
|
||||
viewData,
|
||||
snippets,
|
||||
scripts,
|
||||
insertIntoArticle,
|
||||
insertSnippet,
|
||||
showUpdateMedia,
|
||||
showMediaDetails,
|
||||
processSnippet,
|
||||
onDelete,
|
||||
}: React.PropsWithChildren<IItemMenuProps>) => {
|
||||
|
||||
const copyToClipboard = React.useCallback(() => {
|
||||
if (relPath) {
|
||||
messageHandler.send(DashboardMessage.copyToClipboard, parseWinPath(relPath) || '');
|
||||
}
|
||||
}, [relPath]);
|
||||
|
||||
const runCustomScript = React.useCallback((script: CustomScript) => {
|
||||
messageHandler.send(DashboardMessage.runCustomScript, {
|
||||
script,
|
||||
path: media.fsPath
|
||||
});
|
||||
}, [media]);
|
||||
|
||||
const revealMedia = React.useCallback(() => {
|
||||
messageHandler.send(DashboardMessage.revealMedia, {
|
||||
file: media.fsPath,
|
||||
folder: selectedFolder
|
||||
});
|
||||
}, [selectedFolder]);
|
||||
|
||||
const customScriptActions = React.useMemo(() => {
|
||||
return (scripts || [])
|
||||
.filter((script) => script.type === ScriptType.MediaFile && !script.hidden)
|
||||
.map((script) => (
|
||||
<DropdownMenuItem
|
||||
key={script.title}
|
||||
onClick={() => runCustomScript(script)}
|
||||
>
|
||||
<CommandLineIcon className="mr-2 h-4 w-4" aria-hidden={true} />
|
||||
<span>{script.title}</span>
|
||||
</DropdownMenuItem>
|
||||
));
|
||||
}, [scripts]);
|
||||
|
||||
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-2 -mr-2 -mt-2 group-hover/actions:bg-[var(--vscode-sideBar-background)] group-hover/actions:border-[var(--frontmatter-border)]`}>
|
||||
<div className="relative z-10 flex text-left">
|
||||
<div className="hidden group-hover/actions:flex">
|
||||
<QuickAction title={l10n.t(LocalizationKey.dashboardMediaItemMenuItemView)} onClick={showMediaDetails}>
|
||||
<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)} onClick={showUpdateMedia}>
|
||||
<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)
|
||||
}
|
||||
onClick={insertIntoArticle}
|
||||
>
|
||||
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
|
||||
{viewData?.position && snippets.length > 0 && (
|
||||
<QuickAction title={l10n.t(LocalizationKey.commonInsertSnippet)} onClick={insertSnippet}>
|
||||
<CodeBracketIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{
|
||||
relPath && (
|
||||
<QuickAction title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionCopyPath)} onClick={copyToClipboard}>
|
||||
<ClipboardIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
|
||||
<QuickAction
|
||||
title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionDelete)}
|
||||
className={`hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
|
||||
onClick={onDelete}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className='text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]'>
|
||||
<span className="sr-only">{l10n.t(LocalizationKey.commonMenu)}</span>
|
||||
<EllipsisVerticalIcon className="w-4 h-4" aria-hidden="true" />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<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>
|
||||
))
|
||||
}
|
||||
|
||||
{customScriptActions}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DropdownMenuItem onClick={copyToClipboard}>
|
||||
<ClipboardIcon className="mr-2 h-4 w-4" aria-hidden={true} />
|
||||
<span>{l10n.t(LocalizationKey.dashboardMediaItemQuickActionCopyPath)}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{customScriptActions}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -250,7 +250,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (
|
||||
)}
|
||||
|
||||
<List>
|
||||
{allMedia.map((file) => (
|
||||
{allMedia.map((file, idx) => (
|
||||
<Item key={file.fsPath} media={file} />
|
||||
))}
|
||||
</List>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -15,18 +15,16 @@ export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({
|
||||
}: 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={`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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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(--vscode-foreground)] hover:text-[var(--frontmatter-button-hoverBackground)]`, className)}
|
||||
>
|
||||
{children}
|
||||
<span className="sr-only">{title}</span>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './ActionMenuButton';
|
||||
export * from './MenuButton';
|
||||
export * from './MenuItem';
|
||||
export * from './MenuItems';
|
||||
export * from './QuickAction';
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
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 [crntDeeplApiKey, setCrntDeeplApiKey] = React.useState<string>('');
|
||||
|
||||
const onSave = React.useCallback(() => {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
|
||||
key: ExtensionState.Secrets.DeeplApiKey,
|
||||
value: crntDeeplApiKey
|
||||
}).then((apiKey: string) => {
|
||||
setDeeplApiKey(apiKey);
|
||||
});
|
||||
}, [crntDeeplApiKey]);
|
||||
|
||||
const onChange = (_: string, value: string) => {
|
||||
setCrntDeeplApiKey(value);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.DeeplApiKey).then((apiKey: string) => {
|
||||
setDeeplApiKey(apiKey);
|
||||
setCrntDeeplApiKey(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.DeeplApiKey}
|
||||
value={crntDeeplApiKey || ""}
|
||||
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewDeeplIntputPlaceholder)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<div className={`mt-4 flex gap-2`}>
|
||||
<VSCodeButton
|
||||
onClick={onSave}
|
||||
disabled={deeplApiKey === crntDeeplApiKey}>
|
||||
{l10n.t(LocalizationKey.commonSave)}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ export const NewForm: React.FunctionComponent<INewFormProps> = ({
|
||||
<label htmlFor={`title`} className="block text-sm font-medium capitalize">
|
||||
{l10n.t(LocalizationKey.commonTitle)}
|
||||
{' '}
|
||||
<span className={`text-[var(--vscode-editorError-foreground)]`} title="Required field">
|
||||
<span className={`text-[var(--vscode-editorError-foreground)]`} title={l10n.t(LocalizationKey.fieldRequired)}>
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
@@ -72,7 +72,7 @@ export const NewForm: React.FunctionComponent<INewFormProps> = ({
|
||||
<label htmlFor={`snippet`} className="block text-sm font-medium capitalize">
|
||||
{l10n.t(LocalizationKey.dashboardSnippetsViewNewFormSnippetInputSnippetLabel)}
|
||||
{' '}
|
||||
<span className="text-[var(--vscode-editorError-foreground)]" title="Required field">
|
||||
<span className="text-[var(--vscode-editorError-foreground)]" title={l10n.t(LocalizationKey.fieldRequired)}>
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -65,6 +65,10 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
|
||||
if (mediaData[fieldName]) {
|
||||
return mediaData[fieldName];
|
||||
}
|
||||
|
||||
if (mediaData.metadata && mediaData.metadata[fieldName]) {
|
||||
return mediaData.metadata[fieldName];
|
||||
}
|
||||
},
|
||||
[mediaData]
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Settings } from '../../models/Settings';
|
||||
import { Status } from '../../models/Status';
|
||||
import { Step } from './Step';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { MenuItem } from '../Menu';
|
||||
import { Framework, StaticFolder, Template } from '../../../models';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
@@ -19,6 +18,7 @@ import { Spinner } from '../Common/Spinner';
|
||||
import { AstroContentTypes } from '../Configuration/Astro/AstroContentTypes';
|
||||
import { ContentFolders } from '../Configuration/Common/ContentFolders';
|
||||
import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react';
|
||||
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuSeparator } from '../../../components/shadcn/Dropdown';
|
||||
|
||||
export interface IStepsToGetStartedProps {
|
||||
settings: Settings;
|
||||
@@ -116,42 +116,33 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
{l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedFrameworkDescription)}
|
||||
</div>
|
||||
|
||||
<Menu as="div" className="relative inline-block text-left mt-4">
|
||||
<div>
|
||||
<Menu.Button className={`group flex justify-center p-2 rounded-md border text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]`}>
|
||||
{framework ? framework : l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedFrameworkSelect)}
|
||||
<ChevronDownIcon
|
||||
className={`flex-shrink-0 -mr-1 ml-1 h-5 w-5`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className='mt-4 group flex justify-center p-2 rounded-md border text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)] focus:outline-none'>
|
||||
<span className="">{framework ? framework : l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedFrameworkSelect)}</span>
|
||||
<ChevronDownIcon className="-mr-1 ml-1 w-4 h-4" aria-hidden="true" />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<Menu.Items
|
||||
className={`w-40 origin-top-left absolute left-0 z-10 mt-2 rounded-md 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">
|
||||
<DropdownMenuContent align='start'>
|
||||
<MenuItem
|
||||
title={l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedFrameworkSelectOther)}
|
||||
value={`other`}
|
||||
isCurrent={!framework}
|
||||
onClick={(value: string) => setFrameworkAndSendMessage(value)}
|
||||
/>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{frameworks.map((f) => (
|
||||
<MenuItem
|
||||
title={l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedFrameworkSelectOther)}
|
||||
value={`other`}
|
||||
isCurrent={!framework}
|
||||
key={f.name}
|
||||
title={f.name}
|
||||
value={f.name}
|
||||
isCurrent={f.name === framework}
|
||||
onClick={(value: string) => setFrameworkAndSendMessage(value)}
|
||||
/>
|
||||
|
||||
<hr className={`border-[var(--frontmatter-border)]`} />
|
||||
|
||||
{frameworks.map((f) => (
|
||||
<MenuItem
|
||||
key={f.name}
|
||||
title={f.name}
|
||||
value={f.name}
|
||||
isCurrent={f.name === framework}
|
||||
onClick={(value: string) => setFrameworkAndSendMessage(value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
show: true,
|
||||
@@ -255,7 +246,7 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
</div>
|
||||
),
|
||||
show: isGitRepo,
|
||||
status: settings.git.actions ? Status.Completed : Status.NotStarted
|
||||
status: settings.git?.actions ? Status.Completed : Status.NotStarted
|
||||
},
|
||||
{
|
||||
id: `welcome-import`,
|
||||
@@ -293,12 +284,12 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
}, [settings.crntFramework, settings.framework]);
|
||||
|
||||
React.useEffect(() => {
|
||||
messageHandler.request<boolean>(GeneralCommands.toVSCode.gitIsRepo).then((result) => {
|
||||
messageHandler.request<boolean>(GeneralCommands.toVSCode.git.isRepo).then((result) => {
|
||||
setIsGitRepo(result);
|
||||
});
|
||||
|
||||
setIsGitEnabled(settings.git.actions);
|
||||
}, [settings.git.actions]);
|
||||
setIsGitEnabled(settings.git?.actions || false);
|
||||
}, [settings.git?.actions]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchTemplates = async () => {
|
||||
|
||||
@@ -82,13 +82,13 @@ export const TaxonomyActions: React.FunctionComponent<ITaxonomyActionsProps> = (
|
||||
)}
|
||||
|
||||
<LinkButton
|
||||
title={`Tag content`}
|
||||
title={l10n.t(LocalizationKey.dashboardTaxonomyViewButtonTagTitle)}
|
||||
onClick={onTagging}>
|
||||
<div className='relative'>
|
||||
<TagIcon className={`w-4 h-4`} aria-hidden={true} />
|
||||
<PlusCircleIcon className={`w-3 h-3 absolute left-[-3px] bottom-[-4px] border-1 bg-[var(--vscode-editor-background)] rounded-full`} aria-hidden={true} />
|
||||
</div>
|
||||
<span className="sr-only">{l10n.t(LocalizationKey.commonEdit)}</span>
|
||||
<span className="sr-only">{l10n.t(LocalizationKey.dashboardTaxonomyViewButtonTagTitle)}</span>
|
||||
</LinkButton>
|
||||
|
||||
<LinkButton
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
FilterValuesAtom,
|
||||
FiltersAtom,
|
||||
FolderSelector,
|
||||
LocaleAtom,
|
||||
LocalesAtom,
|
||||
SearchSelector,
|
||||
SettingsSelector,
|
||||
SortingAtom,
|
||||
@@ -22,20 +24,25 @@ import { parseWinPath } from '../../helpers/parseWinPath';
|
||||
import { sortPages } from '../../utils/sortPages';
|
||||
import { ExtensionState } from '../../constants';
|
||||
import { SortingOption } from '../models';
|
||||
import { I18nConfig } from '../../models';
|
||||
import { usePrevious } from '../../panelWebView/hooks/usePrevious';
|
||||
|
||||
export default function usePages(pages: Page[]) {
|
||||
const [pageItems, setPageItems] = useRecoilState(AllPagesAtom);
|
||||
const [sortedPages, setSortedPages] = useState<Page[]>([]);
|
||||
const [pageItems, setPageItems] = useRecoilState(AllPagesAtom);
|
||||
const [sorting, setSorting] = useRecoilState(SortingAtom);
|
||||
const [tabInfo, setTabInfo] = useRecoilState(TabInfoAtom);
|
||||
const [locales, setLocales] = useRecoilState(LocalesAtom);
|
||||
const [, setFilterValues] = useRecoilState(FilterValuesAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const tab = useRecoilValue(TabSelector);
|
||||
const folder = useRecoilValue(FolderSelector);
|
||||
const search = useRecoilValue(SearchSelector);
|
||||
const tag = useRecoilValue(TagSelector);
|
||||
const locale = useRecoilValue(LocaleAtom);
|
||||
const category = useRecoilValue(CategorySelector);
|
||||
const filters = useRecoilValue(FiltersAtom);
|
||||
const tabPrevious = usePrevious(tab);
|
||||
|
||||
/**
|
||||
* Process all the pages by applying the sorting, filtering and searching.
|
||||
@@ -90,6 +97,11 @@ export default function usePages(pages: Page[]) {
|
||||
);
|
||||
}
|
||||
|
||||
// If filtered by locale
|
||||
if (locale) {
|
||||
pagesSorted = pagesSorted.filter((page) => page.fmLocale && page.fmLocale.locale === locale);
|
||||
}
|
||||
|
||||
const filterNames = Object.keys(filters);
|
||||
if (filterNames.length > 0) {
|
||||
for (const filter of filterNames) {
|
||||
@@ -102,7 +114,7 @@ export default function usePages(pages: Page[]) {
|
||||
|
||||
setSortedPages(pagesSorted);
|
||||
},
|
||||
[settings, tab, folder, search, tag, category, sorting, tabInfo, filters]
|
||||
[settings, tab, folder, search, tag, category, locale, sorting, tabInfo, filters]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -114,6 +126,24 @@ export default function usePages(pages: Page[]) {
|
||||
|
||||
let crntPages: Page[] = Object.assign([], pages);
|
||||
|
||||
// Update the translations of pages
|
||||
crntPages = crntPages.map((page) => {
|
||||
if (page.fmTranslations) {
|
||||
const translations = Object.assign({}, page.fmTranslations);
|
||||
|
||||
for (const [key, value] of Object.entries(translations)) {
|
||||
const translatedPage = crntPages.find((p) => parseWinPath(p.fmFilePath).toLowerCase() === parseWinPath(value.path).toLowerCase());
|
||||
if (!translatedPage) {
|
||||
delete translations[key];
|
||||
}
|
||||
}
|
||||
|
||||
return { ...page, fmTranslations: translations };
|
||||
}
|
||||
|
||||
return page;
|
||||
});
|
||||
|
||||
// Process the tab data
|
||||
const draftTypes = Object.assign({}, tabInfo);
|
||||
draftTypes[Tab.All] = crntPages.length;
|
||||
@@ -190,10 +220,21 @@ export default function usePages(pages: Page[]) {
|
||||
}
|
||||
}
|
||||
|
||||
if (tabPrevious !== tab || !locales || locales.length === 0) {
|
||||
// Store the locale information
|
||||
const config: I18nConfig[] = [];
|
||||
crntPages.forEach((page) => {
|
||||
if (page.fmLocale && !config.some(locale => locale.locale === page.fmLocale?.locale)) {
|
||||
config.push(page.fmLocale);
|
||||
}
|
||||
});
|
||||
setLocales(config);
|
||||
}
|
||||
|
||||
// Set the pages
|
||||
setPageItems(crntPages);
|
||||
},
|
||||
[tab, tabInfo, settings, filters]
|
||||
[tab, tabInfo, settings, filters, locales, tabPrevious]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -235,7 +276,7 @@ export default function usePages(pages: Page[]) {
|
||||
} else {
|
||||
startPageProcessing();
|
||||
}
|
||||
}, [settings?.draftField, pages, sorting, search, tag, category, filters, folder]);
|
||||
}, [settings?.draftField, pages, sorting, search, tag, category, locale, filters, folder]);
|
||||
|
||||
useEffect(() => {
|
||||
processByTab(sortedPages);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { I18nConfig } from '../../models';
|
||||
|
||||
export interface Page {
|
||||
// Properties for caching
|
||||
fmCachePath: string;
|
||||
@@ -19,6 +21,16 @@ export interface Page {
|
||||
fmContentType: string;
|
||||
fmDateFormat: string | undefined;
|
||||
|
||||
// i18n fields
|
||||
fmDefaultLocale?: boolean;
|
||||
fmLocale?: I18nConfig;
|
||||
fmTranslations?: {
|
||||
[locale: string]: {
|
||||
locale: I18nConfig;
|
||||
path: string;
|
||||
}
|
||||
};
|
||||
|
||||
title: string;
|
||||
slug: string;
|
||||
date: string | Date;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DraftField,
|
||||
Framework,
|
||||
GitSettings,
|
||||
MediaContentType,
|
||||
Project,
|
||||
Snippets,
|
||||
SortingSetting
|
||||
@@ -19,7 +20,7 @@ import { DataFile } from '../../models/DataFile';
|
||||
export interface Settings {
|
||||
projects: Project[];
|
||||
project: Project;
|
||||
git: GitSettings;
|
||||
git: GitSettings | undefined;
|
||||
beta: boolean;
|
||||
initialized: boolean;
|
||||
wsFolder: string;
|
||||
@@ -47,6 +48,11 @@ export interface Settings {
|
||||
snippetsWrapper: boolean;
|
||||
date: { format: string };
|
||||
lastUpdated: number;
|
||||
media: MediaDashboardSettings;
|
||||
}
|
||||
|
||||
export interface MediaDashboardSettings {
|
||||
contentTypes: MediaContentType[];
|
||||
}
|
||||
|
||||
export interface DashboardState {
|
||||
|
||||
8
src/dashboardWebView/state/atom/LocaleAtom.ts
Normal file
8
src/dashboardWebView/state/atom/LocaleAtom.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const DEFAULT_LOCALE_STATE = '';
|
||||
|
||||
export const LocaleAtom = atom<string | null>({
|
||||
key: 'LocaleAtom',
|
||||
default: DEFAULT_LOCALE_STATE
|
||||
});
|
||||
7
src/dashboardWebView/state/atom/LocalesAtom.ts
Normal file
7
src/dashboardWebView/state/atom/LocalesAtom.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { atom } from 'recoil';
|
||||
import { I18nConfig } from '../../../models';
|
||||
|
||||
export const LocalesAtom = atom<I18nConfig[] | undefined>({
|
||||
key: 'LocalesAtom',
|
||||
default: undefined
|
||||
});
|
||||
@@ -9,6 +9,8 @@ export * from './FolderAtom';
|
||||
export * from './GroupingAtom';
|
||||
export * from './LightboxAtom';
|
||||
export * from './LoadingAtom';
|
||||
export * from './LocaleAtom';
|
||||
export * from './LocalesAtom';
|
||||
export * from './MediaFoldersAtom';
|
||||
export * from './MediaTotalAtom';
|
||||
export * from './ModeAtom';
|
||||
|
||||
20
src/dashboardWebView/utils/getRelPath.ts
Normal file
20
src/dashboardWebView/utils/getRelPath.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { parseWinPath } from '../../helpers/parseWinPath';
|
||||
|
||||
export const getRelPath = (path: string, staticFolder?: string, wsFolder?: string) => {
|
||||
let relPath: string | undefined = '';
|
||||
if (wsFolder && path) {
|
||||
const wsFolderParsed = parseWinPath(wsFolder);
|
||||
const mediaParsed = parseWinPath(path);
|
||||
|
||||
relPath = mediaParsed.split(wsFolderParsed).pop();
|
||||
|
||||
// If the static folder is the root, we can just return the relative path
|
||||
if (staticFolder === '/') {
|
||||
return relPath;
|
||||
} else if (staticFolder && relPath) {
|
||||
const staticFolderParsed = parseWinPath(staticFolder);
|
||||
relPath = relPath.split(staticFolderParsed).pop();
|
||||
}
|
||||
}
|
||||
return relPath;
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './getRelPath';
|
||||
export * from './preserveColor';
|
||||
export * from './updateCssVariables';
|
||||
|
||||
@@ -13,11 +13,10 @@ import {
|
||||
} from './helpers';
|
||||
import ContentProvider from './providers/ContentProvider';
|
||||
import { PagesListener } from './listeners/dashboard';
|
||||
import { NavigationType } from './dashboardWebView/models';
|
||||
import { ModeSwitch } from './services/ModeSwitch';
|
||||
import { PagesParser } from './services/PagesParser';
|
||||
import { ContentType, Telemetry, Extension } from './helpers';
|
||||
import { TaxonomyType, DashboardData } from './models';
|
||||
import { TaxonomyType } from './models';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import {
|
||||
Backers,
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
} from './commands';
|
||||
import { join } from 'path';
|
||||
import { Terminal } from './services';
|
||||
import { i18n } from './commands/i18n';
|
||||
|
||||
let pageUpdateDebouncer: { (fnc: any, time: number): void };
|
||||
let editDebounce: { (fnc: any, time: number): void };
|
||||
@@ -87,51 +87,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// Pages dashboard
|
||||
Dashboard.init();
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.dashboard, (data?: DashboardData) => {
|
||||
Telemetry.send(TelemetryEvent.openContentDashboard);
|
||||
if (!data) {
|
||||
Dashboard.open({ type: NavigationType.Contents });
|
||||
} else {
|
||||
Dashboard.open(data);
|
||||
}
|
||||
})
|
||||
);
|
||||
Dashboard.registerCommands();
|
||||
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.dashboardMedia, (data?: DashboardData) => {
|
||||
Telemetry.send(TelemetryEvent.openMediaDashboard);
|
||||
Dashboard.open({ type: NavigationType.Media });
|
||||
})
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.dashboardSnippets, (data?: DashboardData) => {
|
||||
Telemetry.send(TelemetryEvent.openSnippetsDashboard);
|
||||
Dashboard.open({ type: NavigationType.Snippets });
|
||||
})
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.dashboardData, (data?: DashboardData) => {
|
||||
Telemetry.send(TelemetryEvent.openDataDashboard);
|
||||
Dashboard.open({ type: NavigationType.Data });
|
||||
})
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.dashboardTaxonomy, (data?: DashboardData) => {
|
||||
Telemetry.send(TelemetryEvent.openTaxonomyDashboard);
|
||||
Dashboard.open({ type: NavigationType.Taxonomy });
|
||||
})
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.dashboardClose, (data?: DashboardData) => {
|
||||
Telemetry.send(TelemetryEvent.closeDashboard);
|
||||
Dashboard.close();
|
||||
})
|
||||
);
|
||||
i18n.register();
|
||||
|
||||
if (!extension.getVersion().usedVersion) {
|
||||
vscode.commands.executeCommand(COMMAND_NAME.dashboard);
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
import { format, parse } from 'date-fns';
|
||||
import { Notifications } from './Notifications';
|
||||
import { Article } from '../commands';
|
||||
import { join } from 'path';
|
||||
import { join, parse as parseFile } from 'path';
|
||||
import { EditorHelper } from '@estruyf/vscode';
|
||||
import sanitize from '../helpers/Sanitize';
|
||||
import { ContentType as IContentType } from '../models';
|
||||
@@ -49,7 +49,7 @@ import { Link, Parent } from 'mdast-util-from-markdown/lib';
|
||||
import { Content } from 'mdast';
|
||||
import { CustomScript } from './CustomScript';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { existsAsync, readFileAsync } from '../utils';
|
||||
import { existsAsync } from '../utils';
|
||||
import { mkdirAsync } from '../utils/mkdirAsync';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
@@ -123,8 +123,14 @@ export class ArticleHelper {
|
||||
* Retrieve the file's front matter by its path
|
||||
* @param filePath
|
||||
*/
|
||||
public static async getFrontMatterByPath(filePath: string) {
|
||||
const file = await readFileAsync(filePath, { encoding: 'utf-8' });
|
||||
public static async getFrontMatterByPath(
|
||||
filePath: string
|
||||
): Promise<ParsedFrontMatter | undefined> {
|
||||
const file = await ArticleHelper.getContents(filePath);
|
||||
if (!file) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const article = ArticleHelper.parseFile(file, filePath);
|
||||
if (!article) {
|
||||
return undefined;
|
||||
@@ -136,6 +142,20 @@ export class ArticleHelper {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of a file asynchronously.
|
||||
* @param filePath - The path of the file to read.
|
||||
* @returns A promise that resolves to the contents of the file, or undefined if the file does not exist.
|
||||
*/
|
||||
public static async getContents(filePath: string): Promise<string | undefined> {
|
||||
const file = await workspace.fs.readFile(Uri.file(parseWinPath(filePath)));
|
||||
if (!file) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the new information in the file
|
||||
*
|
||||
@@ -282,6 +302,35 @@ export class ArticleHelper {
|
||||
return isSupportedLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given file path represents a page bundle.
|
||||
*
|
||||
* @param filePath - The path of the file to check.
|
||||
* @returns A boolean indicating whether the file is a page bundle or not.
|
||||
*/
|
||||
public static async isPageBundle(filePath: string) {
|
||||
let article = await ArticleHelper.getFrontMatterByPath(filePath);
|
||||
if (!article) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const contentType = ArticleHelper.getContentType(article);
|
||||
return !!contentType.pageBundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the page folder from the given bundle file path.
|
||||
*
|
||||
* @param filePath - The file path of the bundle.
|
||||
* @returns The page folder path.
|
||||
*/
|
||||
public static getPageFolderFromBundlePath(filePath: string) {
|
||||
// Remove the last folder from the dir
|
||||
const dir = parseFile(filePath).dir;
|
||||
const lastSlash = dir.lastIndexOf('/');
|
||||
return dir.substring(0, lastSlash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date from front matter
|
||||
*/
|
||||
@@ -370,21 +419,9 @@ export class ArticleHelper {
|
||||
if (article.data.type) {
|
||||
contentType = contentTypes.find((ct) => ct.name === article.data.type);
|
||||
} else if (!contentType && article.path) {
|
||||
// Get the content type by the folder name
|
||||
let folders = Folders.get();
|
||||
let parsedPath = parseWinPath(article.path);
|
||||
let pageFolderMatches = folders.filter(
|
||||
(folder) => parsedPath && folder.path && parsedPath.includes(folder.path)
|
||||
);
|
||||
|
||||
// Sort by longest path
|
||||
pageFolderMatches = pageFolderMatches.sort((a, b) => b.path.length - a.path.length);
|
||||
if (
|
||||
pageFolderMatches.length > 0 &&
|
||||
pageFolderMatches[0].contentTypes &&
|
||||
pageFolderMatches[0].contentTypes.length === 1
|
||||
) {
|
||||
const contentTypeName = pageFolderMatches[0].contentTypes[0];
|
||||
const pageFolder = Folders.getPageFolderByFilePath(article.path);
|
||||
if (pageFolder && pageFolder.contentTypes?.length === 1) {
|
||||
const contentTypeName = pageFolder.contentTypes[0];
|
||||
contentType = contentTypes.find((ct) => ct.name === contentTypeName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1003,7 +1003,11 @@ export class ContentType {
|
||||
|
||||
if (field.name === 'title') {
|
||||
if (field.default) {
|
||||
data[field.name] = processArticlePlaceholdersFromData(field.default, data, contentType);
|
||||
data[field.name] = processArticlePlaceholdersFromData(
|
||||
field.default as string,
|
||||
data,
|
||||
contentType
|
||||
);
|
||||
data[field.name] = processTimePlaceholders(
|
||||
data[field.name],
|
||||
field.dateFormat || dateFormat
|
||||
@@ -1037,20 +1041,30 @@ export class ContentType {
|
||||
const defaultValue = field.default;
|
||||
|
||||
if (typeof defaultValue === 'string') {
|
||||
data[field.name] = processArticlePlaceholdersFromData(
|
||||
data[field.name] = await ContentType.processFieldPlaceholders(
|
||||
defaultValue,
|
||||
data,
|
||||
contentType
|
||||
);
|
||||
data[field.name] = processTimePlaceholders(
|
||||
data[field.name],
|
||||
field.dateFormat || dateFormat
|
||||
);
|
||||
data[field.name] = await ArticleHelper.processCustomPlaceholders(
|
||||
data[field.name],
|
||||
contentType,
|
||||
field.dateFormat || dateFormat,
|
||||
titleValue,
|
||||
filePath
|
||||
);
|
||||
} else if (defaultValue && Array.isArray(defaultValue)) {
|
||||
let defaultValues = [];
|
||||
for (let value of defaultValue as string[]) {
|
||||
if (typeof value === 'string') {
|
||||
value = await ContentType.processFieldPlaceholders(
|
||||
value,
|
||||
data,
|
||||
contentType,
|
||||
field.dateFormat || dateFormat,
|
||||
titleValue,
|
||||
filePath
|
||||
);
|
||||
}
|
||||
defaultValues.push(value);
|
||||
}
|
||||
data[field.name] = defaultValues;
|
||||
} else if (typeof defaultValue !== 'undefined') {
|
||||
data[field.name] = defaultValue;
|
||||
} else {
|
||||
@@ -1095,6 +1109,7 @@ export class ContentType {
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
case 'slug':
|
||||
case 'image':
|
||||
case 'file':
|
||||
default:
|
||||
@@ -1113,6 +1128,32 @@ export class ContentType {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the field placeholders in the given value.
|
||||
*
|
||||
* @param defaultValue - The default value for the field.
|
||||
* @param data - The data object containing the field values.
|
||||
* @param contentType - The content type object.
|
||||
* @param dateFormat - The date format string.
|
||||
* @param title - The title string.
|
||||
* @param filePath - The file path string.
|
||||
* @returns The processed value with field placeholders replaced.
|
||||
*/
|
||||
private static async processFieldPlaceholders(
|
||||
defaultValue: string,
|
||||
data: any,
|
||||
contentType: IContentType,
|
||||
dateFormat: string,
|
||||
title: string,
|
||||
filePath: string
|
||||
) {
|
||||
let value = processArticlePlaceholdersFromData(defaultValue, data, contentType);
|
||||
value = processTimePlaceholders(value, dateFormat);
|
||||
value = await ArticleHelper.processCustomPlaceholders(value, title, filePath);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if the content type feature is enabled
|
||||
* @returns
|
||||
|
||||
@@ -30,14 +30,23 @@ import {
|
||||
SETTING_DASHBOARD_CONTENT_CARD_TITLE,
|
||||
SETTING_DASHBOARD_CONTENT_CARD_STATE,
|
||||
SETTING_DASHBOARD_CONTENT_CARD_DESCRIPTION,
|
||||
SETTING_WEBSITE_URL
|
||||
SETTING_WEBSITE_URL,
|
||||
SETTING_MEDIA_CONTENTTYPES
|
||||
} from '../constants';
|
||||
import {
|
||||
DashboardViewType,
|
||||
SortingOption,
|
||||
Settings as ISettings
|
||||
} from '../dashboardWebView/models';
|
||||
import { CustomScript, DraftField, Snippets, SortingSetting, TaxonomyType } from '../models';
|
||||
import {
|
||||
CustomScript,
|
||||
DEFAULT_MEDIA_CONTENT_TYPE,
|
||||
DraftField,
|
||||
MediaContentType,
|
||||
Snippets,
|
||||
SortingSetting,
|
||||
TaxonomyType
|
||||
} from '../models';
|
||||
import { DataFile } from '../models/DataFile';
|
||||
import { DataFolder } from '../models/DataFolder';
|
||||
import { DataType } from '../models/DataType';
|
||||
@@ -79,16 +88,12 @@ export class DashboardSettings {
|
||||
const ext = Extension.getInstance();
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const isInitialized = await Project.isInitialized();
|
||||
const gitActions = Settings.get<boolean>(SETTING_GIT_ENABLED);
|
||||
const pagination = Settings.get<boolean | number>(SETTING_DASHBOARD_CONTENT_PAGINATION);
|
||||
|
||||
const settings = {
|
||||
projects: Settings.getProjects(),
|
||||
project: Settings.getProject(),
|
||||
git: {
|
||||
isGitRepo: gitActions ? await GitListener.isGitRepository() : false,
|
||||
actions: gitActions || false
|
||||
},
|
||||
git: await GitListener.getSettings(),
|
||||
beta: ext.isBetaVersion(),
|
||||
wsFolder: wsFolder ? wsFolder.fsPath : '',
|
||||
staticFolder: Folders.getStaticFolderRelativePath(),
|
||||
@@ -152,6 +157,11 @@ export class DashboardSettings {
|
||||
snippetsWrapper: Settings.get<boolean>(SETTING_SNIPPETS_WRAPPER),
|
||||
isBacker: await ext.getState<boolean | undefined>(CONTEXT.backer, 'global'),
|
||||
websiteUrl: Settings.get<string>(SETTING_WEBSITE_URL),
|
||||
media: {
|
||||
contentTypes: Settings.get<MediaContentType[]>(SETTING_MEDIA_CONTENTTYPES) || [
|
||||
DEFAULT_MEDIA_CONTENT_TYPE
|
||||
]
|
||||
},
|
||||
lastUpdated: new Date().getTime()
|
||||
} as ISettings;
|
||||
|
||||
|
||||
@@ -424,6 +424,14 @@ export class Extension {
|
||||
}
|
||||
}
|
||||
|
||||
public async getSecret(key: string): Promise<string | undefined> {
|
||||
return this.ctx.secrets.get(key);
|
||||
}
|
||||
|
||||
public async setSecret(key: string, value: string): Promise<void> {
|
||||
return this.ctx.secrets.store(key, value);
|
||||
}
|
||||
|
||||
public isBetaVersion() {
|
||||
return basename(this.ctx.globalStorageUri.fsPath) === EXTENSION_BETA_ID;
|
||||
}
|
||||
|
||||
@@ -151,6 +151,60 @@ export class FrameworkDetector {
|
||||
return parseWinPath(relAssetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path by combining the relative path and the file path.
|
||||
* If a static folder is configured, it will be taken into account.
|
||||
* @param relAssetPath The relative path.
|
||||
* @param filePath The file path.
|
||||
* @returns The absolute path.
|
||||
*/
|
||||
public static getAbsPathByFile(relAssetPath: string, filePath: string): string {
|
||||
const staticFolderValue = Settings.get<string | StaticFolder>(SETTING_CONTENT_STATIC_FOLDER);
|
||||
const staticFolder = Folders.getStaticFolderRelativePath();
|
||||
|
||||
if (
|
||||
staticFolderValue &&
|
||||
staticFolder &&
|
||||
typeof staticFolderValue !== 'string' &&
|
||||
staticFolderValue.relative
|
||||
) {
|
||||
const fileDir = dirname(filePath);
|
||||
return parseWinPath(join(fileDir, relAssetPath));
|
||||
}
|
||||
|
||||
return relAssetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative path of an asset file based on the provided absolute asset path and file path.
|
||||
* If the static folder setting is configured and the static folder is available, the relative path is calculated based on the file and asset directories.
|
||||
* Otherwise, the absolute asset path is returned as is.
|
||||
*
|
||||
* @param absAssetPath The absolute path of the asset file.
|
||||
* @param fileDir The path of the directory
|
||||
* @returns The relative path of the asset file.
|
||||
*/
|
||||
public static getRelPathByFileDir(absAssetPath: string, fileDir: string): string {
|
||||
const staticFolderValue = Settings.get<string | StaticFolder>(SETTING_CONTENT_STATIC_FOLDER);
|
||||
const staticFolder = Folders.getStaticFolderRelativePath();
|
||||
|
||||
if (
|
||||
staticFolderValue &&
|
||||
staticFolder &&
|
||||
typeof staticFolderValue !== 'string' &&
|
||||
staticFolderValue.relative
|
||||
) {
|
||||
const assetDir = dirname(absAssetPath);
|
||||
const fileName = parse(absAssetPath);
|
||||
|
||||
let relAssetPath = relative(fileDir, assetDir);
|
||||
relAssetPath = join(relAssetPath, `${fileName.name}${fileName.ext}`);
|
||||
return parseWinPath(relAssetPath);
|
||||
}
|
||||
|
||||
return absAssetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default settings for Hexo
|
||||
*/
|
||||
|
||||
@@ -161,7 +161,9 @@ export class MediaHelpers {
|
||||
dimensions:
|
||||
mimeType && mimeType.startsWith('image/') ? imageSize(file.fsPath) : undefined,
|
||||
mimeType: lookup(file.fsPath) || '',
|
||||
...metadata
|
||||
metadata: {
|
||||
...metadata
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
return { ...file };
|
||||
@@ -478,15 +480,11 @@ export class MediaHelpers {
|
||||
const {
|
||||
file,
|
||||
filename,
|
||||
page,
|
||||
folder,
|
||||
...metadata
|
||||
metadata
|
||||
}: {
|
||||
file: string;
|
||||
filename: string;
|
||||
page: number;
|
||||
folder: string | null;
|
||||
metadata: any;
|
||||
metadata: { [fieldName: string]: string | string[] | Date | number | undefined };
|
||||
} = data;
|
||||
|
||||
const mediaLib = MediaLibrary.getInstance();
|
||||
@@ -522,7 +520,8 @@ export class MediaHelpers {
|
||||
filename: basename(file.fsPath),
|
||||
fsPath: file.fsPath,
|
||||
vsPath: Dashboard.getWebview()?.asWebviewUri(file).toString(),
|
||||
stats: undefined
|
||||
stats: undefined,
|
||||
metadata: {}
|
||||
} as MediaInfo)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
SETTING_SLUG_UPDATE_FILE_NAME,
|
||||
SETTING_TAXONOMY_CUSTOM,
|
||||
SETTING_TAXONOMY_FIELD_GROUPS,
|
||||
SETTING_GIT_ENABLED,
|
||||
SETTING_SEO_TITLE_FIELD
|
||||
} from '../constants';
|
||||
import { GitListener } from '../listeners/general';
|
||||
@@ -49,14 +48,9 @@ import { Folders } from '../commands';
|
||||
|
||||
export class PanelSettings {
|
||||
public static async get(): Promise<IPanelSettings> {
|
||||
const gitActions = Settings.get<boolean>(SETTING_GIT_ENABLED);
|
||||
|
||||
return {
|
||||
aiEnabled: Settings.get<boolean>(SETTING_SPONSORS_AI_ENABLED) || false,
|
||||
git: {
|
||||
isGitRepo: gitActions ? await GitListener.isGitRepository() : false,
|
||||
actions: gitActions || false
|
||||
},
|
||||
git: await GitListener.getSettings(),
|
||||
seo: {
|
||||
title: (Settings.get(SETTING_SEO_TITLE_LENGTH) as number) || -1,
|
||||
slug: (Settings.get(SETTING_SEO_SLUG_LENGTH) as number) || -1,
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import {
|
||||
EXTENSION_NAME,
|
||||
SETTING_CONFIG_DYNAMIC_FILE_PATH,
|
||||
SETTING_PROJECTS
|
||||
} from './../constants/settings';
|
||||
import { parseWinPath } from './parseWinPath';
|
||||
import { Telemetry } from './Telemetry';
|
||||
import { Notifications } from './Notifications';
|
||||
import { commands, Uri, workspace, window } from 'vscode';
|
||||
import * as vscode from 'vscode';
|
||||
import {
|
||||
commands,
|
||||
Uri,
|
||||
workspace,
|
||||
window,
|
||||
WorkspaceConfiguration,
|
||||
FileSystemWatcher,
|
||||
Disposable,
|
||||
ProgressLocation
|
||||
} from 'vscode';
|
||||
import { ContentType, CustomTaxonomy, Project } from '../models';
|
||||
import {
|
||||
SETTING_TAXONOMY_TAGS,
|
||||
SETTING_TAXONOMY_CATEGORIES,
|
||||
EXTENSION_NAME,
|
||||
CONFIG_KEY,
|
||||
CONTEXT,
|
||||
ExtensionState,
|
||||
@@ -35,8 +37,13 @@ import {
|
||||
SETTING_GLOBAL_NOTIFICATIONS,
|
||||
SETTING_GLOBAL_NOTIFICATIONS_DISABLED,
|
||||
SETTING_MEDIA_SUPPORTED_MIMETYPES,
|
||||
SETTING_MEDIA_CONTENTTYPES,
|
||||
SETTING_COMMA_SEPARATED_FIELDS,
|
||||
SETTING_REMOVE_QUOTES
|
||||
SETTING_REMOVE_QUOTES,
|
||||
SETTING_CONFIG_DYNAMIC_FILE_PATH,
|
||||
SETTING_PROJECTS,
|
||||
SETTING_TAXONOMY_TAGS,
|
||||
SETTING_TAXONOMY_CATEGORIES
|
||||
} from '../constants';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { join, basename, dirname, parse } from 'path';
|
||||
@@ -59,13 +66,13 @@ export class Settings {
|
||||
public static globalConfigFolder = '.frontmatter/config';
|
||||
public static globalConfigPath: string | undefined = undefined;
|
||||
public static globalConfig: any;
|
||||
private static config: vscode.WorkspaceConfiguration;
|
||||
private static config: WorkspaceConfiguration;
|
||||
private static isInitialized: boolean = false;
|
||||
private static listeners: { id: string; callback: (global?: any) => void }[] = [];
|
||||
private static fileCreationWatcher: vscode.FileSystemWatcher | undefined;
|
||||
private static fileChangeWatcher: vscode.FileSystemWatcher | undefined;
|
||||
private static fileSaveListener: vscode.Disposable;
|
||||
private static fileDeleteListener: vscode.Disposable;
|
||||
private static fileCreationWatcher: FileSystemWatcher | undefined;
|
||||
private static fileChangeWatcher: FileSystemWatcher | undefined;
|
||||
private static fileSaveListener: Disposable;
|
||||
private static fileDeleteListener: Disposable;
|
||||
private static readConfigPromise: Promise<void> | undefined = undefined;
|
||||
private static project: Project | undefined = undefined;
|
||||
private static configDebouncer = debounceCallback();
|
||||
@@ -109,10 +116,10 @@ export class Settings {
|
||||
commands.registerCommand(COMMAND_NAME.settingsRefresh, Settings.refreshConfig);
|
||||
}
|
||||
|
||||
Settings.config = vscode.workspace.getConfiguration(CONFIG_KEY);
|
||||
Settings.config = workspace.getConfiguration(CONFIG_KEY);
|
||||
|
||||
Settings.attachListener('settings-init', async () => {
|
||||
Settings.config = vscode.workspace.getConfiguration(CONFIG_KEY);
|
||||
Settings.config = workspace.getConfiguration(CONFIG_KEY);
|
||||
});
|
||||
|
||||
Settings.onConfigChange();
|
||||
@@ -674,7 +681,7 @@ export class Settings {
|
||||
try {
|
||||
await window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
location: ProgressLocation.Notification,
|
||||
title: l10n.t(
|
||||
LocalizationKey.helpersSettingsHelperReadConfigProgressTitle,
|
||||
EXTENSION_NAME
|
||||
@@ -866,6 +873,10 @@ export class Settings {
|
||||
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_CONTENT_TYPES)) {
|
||||
Settings.updateGlobalConfigArraySetting(SETTING_TAXONOMY_CONTENT_TYPES, 'name', configJson);
|
||||
}
|
||||
// Media Content types
|
||||
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_MEDIA_CONTENTTYPES)) {
|
||||
Settings.updateGlobalConfigArraySetting(SETTING_MEDIA_CONTENTTYPES, 'name', configJson);
|
||||
}
|
||||
// Data files
|
||||
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_FILES)) {
|
||||
Settings.updateGlobalConfigArraySetting(SETTING_DATA_FILES, 'id', configJson);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Folders } from '../../commands/Folders';
|
||||
import {
|
||||
COMMAND_NAME,
|
||||
ExtensionState,
|
||||
GeneralCommands,
|
||||
SETTING_CONTENT_STATIC_FOLDER,
|
||||
SETTING_FRAMEWORK_ID,
|
||||
SETTING_PREVIEW_HOST
|
||||
@@ -61,9 +62,21 @@ export class SettingsListener extends BaseListener {
|
||||
case DashboardMessage.setSettings:
|
||||
this.setConfigSettings(msg);
|
||||
break;
|
||||
case GeneralCommands.toVSCode.secrets.get:
|
||||
this.getSecretValue(msg.command, msg.payload, msg.requestId);
|
||||
break;
|
||||
case GeneralCommands.toVSCode.secrets.set:
|
||||
this.setSecretValue(msg.command, msg.payload, msg.requestId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the configuration settings based on the provided payload.
|
||||
* @param command - The command to execute.
|
||||
* @param requestId - The ID of the request.
|
||||
* @param payload - The payload containing the settings to retrieve.
|
||||
*/
|
||||
public static async getConfigSettings({ command, requestId, payload }: PostMessageData) {
|
||||
if (!command || !requestId || !payload) {
|
||||
return;
|
||||
@@ -96,6 +109,10 @@ export class SettingsListener extends BaseListener {
|
||||
this.sendRequest(command as any, requestId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the current project to the specified project.
|
||||
* @param project - The name of the project to switch to.
|
||||
*/
|
||||
public static async switchProject(project: string) {
|
||||
if (project) {
|
||||
this.sendMsg(DashboardCommand.loading, 'loading' as LoadingType);
|
||||
@@ -123,6 +140,50 @@ export class SettingsListener extends BaseListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the secret value for a given command and value.
|
||||
* @param command - The command to retrieve the secret value for.
|
||||
* @param key - The key associated with the secret.
|
||||
* @param requestId - Optional. The ID of the request.
|
||||
* @returns A Promise that resolves to the secret value.
|
||||
*/
|
||||
private static async getSecretValue(command: string, key: string, requestId?: string) {
|
||||
if (!command || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = Extension.getInstance();
|
||||
const value = await extension.getSecret(key);
|
||||
|
||||
this.sendRequest(command as any, requestId, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the secret value for a given key.
|
||||
* @param command - The command to execute.
|
||||
* @param key - The key for the secret value.
|
||||
* @param value - The secret value to set.
|
||||
* @param requestId - Optional. The request ID.
|
||||
*/
|
||||
private static async setSecretValue(
|
||||
command: string,
|
||||
{ key, value }: { key: string; value: string },
|
||||
requestId?: string
|
||||
) {
|
||||
if (!command || !requestId || !key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = Extension.getInstance();
|
||||
await extension.setSecret(key, value || '');
|
||||
|
||||
Notifications.info(
|
||||
l10n.t(LocalizationKey.listenersDashboardSettingsListenerSetSecretValueMessage)
|
||||
);
|
||||
|
||||
this.sendRequest(command as any, requestId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a setting from the dashboard
|
||||
* @param data
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Dashboard } from '../../commands/Dashboard';
|
||||
import { PanelProvider } from '../../panelWebView/PanelProvider';
|
||||
import { ArticleHelper, Extension } from '../../helpers';
|
||||
import { Logger } from '../../helpers/Logger';
|
||||
import { commands, Uri, window } from 'vscode';
|
||||
import { commands, Uri, window, workspace } from 'vscode';
|
||||
import { PostMessageData } from '../../models';
|
||||
import { Preview } from '../../commands';
|
||||
import { urlJoin } from 'url-join-ts';
|
||||
@@ -19,6 +19,12 @@ export abstract class BaseListener {
|
||||
case GeneralCommands.toVSCode.openOnWebsite:
|
||||
this.openOnWebsite(msg.payload);
|
||||
break;
|
||||
case GeneralCommands.toVSCode.runCommand:
|
||||
if (msg.payload) {
|
||||
const { command, args } = msg.payload;
|
||||
commands.executeCommand(command, args);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
GIT_CONFIG,
|
||||
SETTING_DATE_FORMAT,
|
||||
SETTING_GIT_COMMIT_MSG,
|
||||
SETTING_GIT_DISABLED_BRANCHES,
|
||||
SETTING_GIT_ENABLED,
|
||||
SETTING_GIT_REQUIRES_COMMIT_MSG,
|
||||
SETTING_GIT_SUBMODULE_BRANCH,
|
||||
SETTING_GIT_SUBMODULE_FOLDER,
|
||||
SETTING_GIT_SUBMODULE_PULL,
|
||||
@@ -19,21 +21,58 @@ import {
|
||||
Extension,
|
||||
Logger,
|
||||
Notifications,
|
||||
parseWinPath,
|
||||
processTimePlaceholders,
|
||||
Telemetry
|
||||
} from '../../helpers';
|
||||
import { GeneralCommands } from './../../constants/GeneralCommands';
|
||||
import simpleGit, { SimpleGit } from 'simple-git';
|
||||
import { Folders } from '../../commands/Folders';
|
||||
import { commands } from 'vscode';
|
||||
import { PostMessageData } from '../../models';
|
||||
import { Event, commands, extensions } from 'vscode';
|
||||
import { GitAPIState, GitRepository, PostMessageData } from '../../models';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../localization';
|
||||
|
||||
export class GitListener {
|
||||
private static gitAPI: {
|
||||
onDidChangeState: Event<GitAPIState>;
|
||||
onDidOpenRepository: Event<GitRepository>;
|
||||
onDidCloseRepository: Event<GitRepository>;
|
||||
getAPI: (version: number) => any;
|
||||
repositories: GitRepository[];
|
||||
} | null = null;
|
||||
private static isRegistered: boolean = false;
|
||||
private static client: SimpleGit | null = null;
|
||||
private static subClient: SimpleGit | null = null;
|
||||
private static repository: GitRepository | null = null;
|
||||
private static branchName: string | null = null;
|
||||
|
||||
/**
|
||||
* Retrieves the Git settings.
|
||||
* @returns {Promise<{
|
||||
* isGitRepo: boolean,
|
||||
* actions: boolean,
|
||||
* disabledBranches: string[],
|
||||
* requiresCommitMessage: string[]
|
||||
* }>} The Git settings.
|
||||
*/
|
||||
public static async getSettings() {
|
||||
const gitActions = Settings.get<boolean>(SETTING_GIT_ENABLED);
|
||||
if (gitActions) {
|
||||
return {
|
||||
isGitRepo: gitActions ? await GitListener.isGitRepository() : false,
|
||||
actions: gitActions || false,
|
||||
disabledBranches: gitActions
|
||||
? Settings.get<string[]>(SETTING_GIT_DISABLED_BRANCHES) || []
|
||||
: [],
|
||||
requiresCommitMessage: gitActions
|
||||
? Settings.get<string[]>(SETTING_GIT_REQUIRES_COMMIT_MSG) || []
|
||||
: []
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the listener
|
||||
@@ -64,11 +103,19 @@ export class GitListener {
|
||||
*/
|
||||
public static process(msg: PostMessageData) {
|
||||
switch (msg.command) {
|
||||
case GeneralCommands.toVSCode.gitIsRepo:
|
||||
this.checkIsGitRepo(msg.command, msg.requestId);
|
||||
case GeneralCommands.toVSCode.git.sync:
|
||||
this.sync(msg.payload);
|
||||
break;
|
||||
case GeneralCommands.toVSCode.gitSync:
|
||||
this.sync();
|
||||
case GeneralCommands.toVSCode.git.fetch:
|
||||
this.sync(undefined, false);
|
||||
break;
|
||||
case GeneralCommands.toVSCode.git.getBranch:
|
||||
this.getBranch(msg.command, msg.requestId);
|
||||
break;
|
||||
case GeneralCommands.toVSCode.git.selectBranch:
|
||||
this.selectBranch();
|
||||
case GeneralCommands.toVSCode.git.isRepo:
|
||||
this.checkIsGitRepo(msg.command, msg.requestId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -83,27 +130,41 @@ export class GitListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the sync
|
||||
* Selects the current branch in the Git repository.
|
||||
* @returns {Promise<void>} A promise that resolves when the branch command has been executed.
|
||||
*/
|
||||
public static async sync() {
|
||||
try {
|
||||
this.sendMsg(GeneralCommands.toWebview.gitSyncingStart, {});
|
||||
public static async selectBranch(): Promise<void> {
|
||||
const workspaceFolder = Folders.getWorkspaceFolder();
|
||||
await commands.executeCommand('git.checkout', workspaceFolder);
|
||||
}
|
||||
|
||||
Telemetry.send(TelemetryEvent.gitSync);
|
||||
/**
|
||||
* Synchronizes the local repository with the remote repository.
|
||||
* @param commitMsg The commit message for the push operation.
|
||||
* @param isSync Determines whether to perform a sync operation (default: true) or a fetch operation.
|
||||
*/
|
||||
public static async sync(commitMsg?: string, isSync: boolean = true) {
|
||||
try {
|
||||
this.sendMsg(GeneralCommands.toWebview.git.syncingStart, isSync ? 'syncing' : 'fetching');
|
||||
|
||||
Telemetry.send(isSync ? TelemetryEvent.gitSync : TelemetryEvent.gitFetch);
|
||||
|
||||
await this.pull();
|
||||
await this.push();
|
||||
|
||||
this.sendMsg(GeneralCommands.toWebview.gitSyncingEnd, {});
|
||||
if (isSync) {
|
||||
await this.push(commitMsg);
|
||||
}
|
||||
|
||||
this.sendMsg(GeneralCommands.toWebview.git.syncingEnd, {});
|
||||
} catch (e) {
|
||||
Logger.error((e as Error).message);
|
||||
this.sendMsg(GeneralCommands.toWebview.gitSyncingEnd, {});
|
||||
this.sendMsg(GeneralCommands.toWebview.git.syncingEnd, {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current workspace is a git repository
|
||||
* @returns
|
||||
* Checks if the current workspace is a Git repository.
|
||||
* @returns A boolean indicating whether the current workspace is a Git repository.
|
||||
*/
|
||||
public static async isGitRepository() {
|
||||
const git = this.getClient();
|
||||
@@ -117,12 +178,15 @@ export class GitListener {
|
||||
Logger.warning(`Current workspace is not a GIT repository`);
|
||||
}
|
||||
|
||||
GitListener.vscodeGitProvider();
|
||||
|
||||
return isRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull the changes from the remote
|
||||
* @returns
|
||||
* Pulls the latest changes from the remote repository.
|
||||
* If submoduleFolder is specified, it checks out the submoduleBranch for the submodule located in that folder.
|
||||
* If submodulePull is true, it also updates the submodules with the latest changes from the remote repository.
|
||||
*/
|
||||
private static async pull() {
|
||||
const git = this.getClient();
|
||||
@@ -157,11 +221,14 @@ export class GitListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Push the changes to the remote
|
||||
* @returns
|
||||
* Pushes the changes to the remote repository.
|
||||
*
|
||||
* @param commitMsg The commit message to use. If not provided, it will use the default commit message or the one specified in the settings.
|
||||
* @returns A promise that resolves when the push operation is completed.
|
||||
*/
|
||||
private static async push() {
|
||||
let commitMsg = Settings.get<string>(SETTING_GIT_COMMIT_MSG);
|
||||
private static async push(commitMsg?: string) {
|
||||
commitMsg =
|
||||
commitMsg || Settings.get<string>(SETTING_GIT_COMMIT_MSG) || 'Synced by Front Matter';
|
||||
|
||||
if (commitMsg) {
|
||||
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
@@ -240,9 +307,11 @@ export class GitListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the git client
|
||||
* @param submoduleFolder
|
||||
* @returns
|
||||
* Retrieves the Git client instance based on the provided submodule folder.
|
||||
* If no submodule folder is provided, it returns the main Git client instance.
|
||||
* If a submodule folder is provided, it returns the submodule-specific Git client instance.
|
||||
* @param submoduleFolder The path to the submodule folder.
|
||||
* @returns The Git client instance or null if it cannot be retrieved.
|
||||
*/
|
||||
private static getClient(submoduleFolder: string = ''): SimpleGit | null {
|
||||
if (!submoduleFolder && this.client) {
|
||||
@@ -269,9 +338,100 @@ export class GitListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the message to the webview
|
||||
* @param command
|
||||
* @param payload
|
||||
* Initializes the VS Code Git provider and sets up event listeners for repository changes.
|
||||
* @returns {Promise<void>} A promise that resolves when the Git provider is initialized.
|
||||
*/
|
||||
private static async vscodeGitProvider(): Promise<void> {
|
||||
if (!GitListener.gitAPI) {
|
||||
const extension = extensions.getExtension('vscode.git');
|
||||
|
||||
/**
|
||||
* Logic from: https://github.com/microsoft/vscode/blob/main/extensions/github/src/extension.ts
|
||||
* initializeGitExtension
|
||||
*/
|
||||
if (extension) {
|
||||
const gitExtension = extension.isActive ? extension.exports : await extension.activate();
|
||||
|
||||
// Get version 1 of the API
|
||||
GitListener.gitAPI = gitExtension.getAPI(1);
|
||||
|
||||
if (!GitListener.gitAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
GitListener.listenToRepo(GitListener.gitAPI?.repositories);
|
||||
|
||||
GitListener.gitAPI.onDidChangeState(() => {
|
||||
GitListener.listenToRepo(GitListener.gitAPI?.repositories);
|
||||
});
|
||||
|
||||
GitListener.gitAPI.onDidOpenRepository((repo: GitRepository) => {
|
||||
GitListener.triggerBranchChange(repo);
|
||||
});
|
||||
|
||||
GitListener.gitAPI.onDidCloseRepository((repo: GitRepository) => {
|
||||
Logger.info(`Closed repo: ${repo?.state?.HEAD?.name}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the branch name and sends a request.
|
||||
* @param command - The command to send.
|
||||
* @param requestId - The ID of the request.
|
||||
*/
|
||||
private static async getBranch(command: string, requestId?: string) {
|
||||
if (!command || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendRequest(command, requestId, GitListener.repository?.state?.HEAD?.name);
|
||||
}
|
||||
|
||||
private static listenToRepo(repositories: GitRepository[] | undefined) {
|
||||
if (!repositories) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (repositories && repositories.length === 1) {
|
||||
GitListener.triggerBranchChange(repositories[0]);
|
||||
} else if (repositories && repositories.length > 1) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (wsFolder) {
|
||||
const repo = repositories.find(
|
||||
(repo) => parseWinPath(repo.rootUri.fsPath) === parseWinPath(wsFolder.fsPath)
|
||||
);
|
||||
if (repo) {
|
||||
GitListener.triggerBranchChange(repo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a branch change event for the specified Git repository.
|
||||
* @param repo The Git repository to monitor for branch changes.
|
||||
*/
|
||||
private static async triggerBranchChange(repo: GitRepository | null) {
|
||||
if (repo && repo.state) {
|
||||
if (repo.state?.HEAD?.name && repo.state.HEAD.name !== GitListener.branchName) {
|
||||
GitListener.branchName = repo.state.HEAD.name;
|
||||
GitListener.repository = repo;
|
||||
|
||||
this.sendMsg(GeneralCommands.toWebview.git.branchName, GitListener.branchName);
|
||||
|
||||
repo.state.onDidChange(() => {
|
||||
GitListener.triggerBranchChange(repo);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the panel and the dashboard.
|
||||
* @param command - The command to send.
|
||||
* @param payload - The payload to send with the command.
|
||||
*/
|
||||
private static sendMsg(command: string, payload: any) {
|
||||
const extPath = Extension.getInstance().extensionPath;
|
||||
@@ -281,4 +441,23 @@ export class GitListener {
|
||||
|
||||
Dashboard.postWebviewMessage({ command: command as any, payload });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the webview panel.
|
||||
* @param command - The command to send.
|
||||
* @param requestId - The unique identifier for the request.
|
||||
* @param payload - The payload to send with the request.
|
||||
*/
|
||||
private static sendRequest(command: string, requestId: string, payload: any) {
|
||||
const extPath = Extension.getInstance().extensionPath;
|
||||
const panel = PanelProvider.getInstance(extPath);
|
||||
|
||||
panel.getWebview()?.postMessage({
|
||||
command,
|
||||
requestId,
|
||||
payload
|
||||
});
|
||||
|
||||
Dashboard.postWebviewMessage({ command: command as any, requestId, payload });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,14 @@ export enum LocalizationKey {
|
||||
* Back
|
||||
*/
|
||||
commonBack = 'common.back',
|
||||
/**
|
||||
* Open
|
||||
*/
|
||||
commonOpen = 'common.open',
|
||||
/**
|
||||
* Open: {0}
|
||||
*/
|
||||
commonOpenWithValue = 'common.openWithValue',
|
||||
/**
|
||||
* Loading content
|
||||
*/
|
||||
@@ -167,6 +175,10 @@ export enum LocalizationKey {
|
||||
* Astro
|
||||
*/
|
||||
settingsViewAstro = 'settings.view.astro',
|
||||
/**
|
||||
* Integration
|
||||
*/
|
||||
settingsViewIntegration = 'settings.view.integration',
|
||||
/**
|
||||
* Open dashboard on startup
|
||||
*/
|
||||
@@ -211,6 +223,10 @@ export enum LocalizationKey {
|
||||
* Read more about Git submodules
|
||||
*/
|
||||
settingsGitSubmoduleLink = 'settings.git.submoduleLink',
|
||||
/**
|
||||
* Integration
|
||||
*/
|
||||
settingsIntegrationTitle = 'settings.integration.title',
|
||||
/**
|
||||
* Website and SSG settings
|
||||
*/
|
||||
@@ -227,6 +243,18 @@ export enum LocalizationKey {
|
||||
* SSG/Framework start command
|
||||
*/
|
||||
settingsCommonSettingsStartCommand = 'settings.commonSettings.startCommand',
|
||||
/**
|
||||
* DeepL
|
||||
*/
|
||||
settingsIntegrationsViewDeeplTitle = 'settings.integrationsView.deepl.title',
|
||||
/**
|
||||
* Authentication key
|
||||
*/
|
||||
settingsIntegrationsViewDeeplIntputLabel = 'settings.integrationsView.deepl.intput.label',
|
||||
/**
|
||||
* Enter your DeepL authentication key
|
||||
*/
|
||||
settingsIntegrationsViewDeeplIntputPlaceholder = 'settings.integrationsView.deepl.intput.placeholder',
|
||||
/**
|
||||
* Developer mode
|
||||
*/
|
||||
@@ -307,6 +335,14 @@ export enum LocalizationKey {
|
||||
* Are you sure you want to delete the "{0}" content?
|
||||
*/
|
||||
dashboardContentsContentActionsAlertDescription = 'dashboard.contents.contentActions.alert.description',
|
||||
/**
|
||||
* Create translation
|
||||
*/
|
||||
dashboardContentsContentActionsTranslationsCreate = 'dashboard.contents.contentActions.translations.create',
|
||||
/**
|
||||
* Translations
|
||||
*/
|
||||
dashboardContentsContentActionsTranslationsMenu = 'dashboard.contents.contentActions.translations.menu',
|
||||
/**
|
||||
* <invalid title>
|
||||
*/
|
||||
@@ -419,6 +455,14 @@ export enum LocalizationKey {
|
||||
* Please close the dashboard and try again.
|
||||
*/
|
||||
dashboardErrorViewDescription = 'dashboard.errorView.description',
|
||||
/**
|
||||
* Locale
|
||||
*/
|
||||
dashboardFiltersLanguageFilterLabel = 'dashboard.filters.languageFilter.label',
|
||||
/**
|
||||
* All
|
||||
*/
|
||||
dashboardFiltersLanguageFilterAll = 'dashboard.filters.languageFilter.all',
|
||||
/**
|
||||
* Home
|
||||
*/
|
||||
@@ -675,6 +719,14 @@ export enum LocalizationKey {
|
||||
* Create new folder
|
||||
*/
|
||||
dashboardMediaFolderCreationFolderCreate = 'dashboard.media.folderCreation.folder.create',
|
||||
/**
|
||||
* Insert image
|
||||
*/
|
||||
dashboardMediaItemButtomInsertImage = 'dashboard.media.item.buttom.insert.image',
|
||||
/**
|
||||
* Insert snippet
|
||||
*/
|
||||
dashboardMediaItemButtomInsertSnippet = 'dashboard.media.item.buttom.insert.snippet',
|
||||
/**
|
||||
* Insert image for your "{0}" field
|
||||
*/
|
||||
@@ -691,6 +743,10 @@ export enum LocalizationKey {
|
||||
* Delete media file
|
||||
*/
|
||||
dashboardMediaItemQuickActionDelete = 'dashboard.media.item.quickAction.delete',
|
||||
/**
|
||||
* View media details
|
||||
*/
|
||||
dashboardMediaItemMenuItemView = 'dashboard.media.item.menuItem.view',
|
||||
/**
|
||||
* Edit metadata
|
||||
*/
|
||||
@@ -975,6 +1031,10 @@ export enum LocalizationKey {
|
||||
* Add {0} to taxonomy settings
|
||||
*/
|
||||
dashboardTaxonomyViewButtonAddTitle = 'dashboard.taxonomyView.button.add.title',
|
||||
/**
|
||||
* Tag content
|
||||
*/
|
||||
dashboardTaxonomyViewButtonTagTitle = 'dashboard.taxonomyView.button.tag.title',
|
||||
/**
|
||||
* Edit {0}
|
||||
*/
|
||||
@@ -1123,6 +1183,22 @@ export enum LocalizationKey {
|
||||
* The following Astro Content Collections can be used to generate a content-type.
|
||||
*/
|
||||
dashboardConfigurationAstroAstroContentTypesDescription = 'dashboard.configuration.astro.astroContentTypes.description',
|
||||
/**
|
||||
* Publish changes
|
||||
*/
|
||||
panelGitGitActionTitle = 'panel.git.gitAction.title',
|
||||
/**
|
||||
* Select branch
|
||||
*/
|
||||
panelGitGitActionBranchSelect = 'panel.git.gitAction.branch.select',
|
||||
/**
|
||||
* Commit message
|
||||
*/
|
||||
panelGitGitActionInputPlaceholder = 'panel.git.gitAction.input.placeholder',
|
||||
/**
|
||||
* Fetch
|
||||
*/
|
||||
panelGitGitActionButtonFetch = 'panel.git.gitAction.button.fetch',
|
||||
/**
|
||||
* Content-type
|
||||
*/
|
||||
@@ -1632,6 +1708,46 @@ export enum LocalizationKey {
|
||||
* Create folder
|
||||
*/
|
||||
commandsFoldersGetNotificationErrorCreateAction = 'commands.folders.get.notificationError.create.action',
|
||||
/**
|
||||
* No file selected.
|
||||
*/
|
||||
commandsI18nCreateWarningNoFileSelected = 'commands.i18n.create.warning.noFileSelected',
|
||||
/**
|
||||
* The file could not be retrieved.
|
||||
*/
|
||||
commandsI18nCreateWarningNoFile = 'commands.i18n.create.warning.noFile',
|
||||
/**
|
||||
* Content type could not be retrieved for the current file.
|
||||
*/
|
||||
commandsI18nCreateWarningNoContentType = 'commands.i18n.create.warning.noContentType',
|
||||
/**
|
||||
* No i18n configuration found.
|
||||
*/
|
||||
commandsI18nCreateWarningNoConfig = 'commands.i18n.create.warning.noConfig',
|
||||
/**
|
||||
* The current file cannot be used for i18n content creation.
|
||||
*/
|
||||
commandsI18nCreateWarningNotDefaultLocale = 'commands.i18n.create.warning.notDefaultLocale',
|
||||
/**
|
||||
* The i18n translation already exists.
|
||||
*/
|
||||
commandsI18nCreateErrorFileExists = 'commands.i18n.create.error.fileExists',
|
||||
/**
|
||||
* Created "{0}" i18n content file.
|
||||
*/
|
||||
commandsI18nCreateSuccessCreated = 'commands.i18n.create.success.created',
|
||||
/**
|
||||
* Create content for locale
|
||||
*/
|
||||
commandsI18nCreateQuickPickTitle = 'commands.i18n.create.quickPick.title',
|
||||
/**
|
||||
* To which locale do you want to create a new content?
|
||||
*/
|
||||
commandsI18nCreateQuickPickPlaceHolder = 'commands.i18n.create.quickPick.placeHolder',
|
||||
/**
|
||||
* Translating content...
|
||||
*/
|
||||
commandsI18nTranslateProgressTitle = 'commands.i18n.translate.progress.title',
|
||||
/**
|
||||
* Preview: {0}
|
||||
*/
|
||||
@@ -2312,6 +2428,10 @@ export enum LocalizationKey {
|
||||
* Failed to initialize the template.
|
||||
*/
|
||||
listenersDashboardSettingsListenerTriggerTemplateInitError = 'listeners.dashboard.settingsListener.triggerTemplate.init.error',
|
||||
/**
|
||||
* Setting has been updated.
|
||||
*/
|
||||
listenersDashboardSettingsListenerSetSecretValueMessage = 'listeners.dashboard.settingsListener.setSecretValue.message',
|
||||
/**
|
||||
* Snippet missing title or body
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { I18nConfig } from './i18nConfig';
|
||||
|
||||
export interface ContentFolder {
|
||||
title: string;
|
||||
path: string;
|
||||
@@ -10,4 +12,6 @@ export interface ContentFolder {
|
||||
originalPath?: string;
|
||||
$schema?: string;
|
||||
extended?: boolean;
|
||||
defaultLocale?: string;
|
||||
locales: I18nConfig[];
|
||||
}
|
||||
|
||||
34
src/models/GitRepository.ts
Normal file
34
src/models/GitRepository.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Event } from 'vscode';
|
||||
|
||||
export type GitAPIState = 'uninitialized' | 'initialized';
|
||||
|
||||
export interface GitRepository {
|
||||
state: GitRepositoryState;
|
||||
rootUri: {
|
||||
fsPath: string;
|
||||
path: string;
|
||||
};
|
||||
repository: {
|
||||
getBranches: () => Promise<GitBranch[]>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GitRepositoryState {
|
||||
HEAD?: GitBranch;
|
||||
onDidChange: Event<void>;
|
||||
}
|
||||
|
||||
export interface GitBranch {
|
||||
type: number;
|
||||
name?: string;
|
||||
upstream: Upstream;
|
||||
commit: string;
|
||||
ahead: number;
|
||||
behind: number;
|
||||
}
|
||||
|
||||
export interface Upstream {
|
||||
name: string;
|
||||
remote: string;
|
||||
commit: string;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export interface GitSettings {
|
||||
isGitRepo: boolean;
|
||||
actions: boolean;
|
||||
disabledBranches: string[];
|
||||
requiresCommitMessage: string[];
|
||||
}
|
||||
|
||||
32
src/models/MediaContentType.ts
Normal file
32
src/models/MediaContentType.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Field } from '.';
|
||||
|
||||
export interface MediaContentType {
|
||||
name: string;
|
||||
fileTypes: string[] | null | undefined;
|
||||
fields: Field[];
|
||||
}
|
||||
|
||||
export const DEFAULT_MEDIA_CONTENT_TYPE: MediaContentType = {
|
||||
name: 'default',
|
||||
fileTypes: null,
|
||||
fields: [
|
||||
{
|
||||
title: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
title: 'Caption',
|
||||
name: 'caption',
|
||||
type: 'string',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
title: 'Alt text',
|
||||
name: 'alt',
|
||||
type: 'string',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -14,11 +14,15 @@ export interface MediaInfo {
|
||||
fsPath: string;
|
||||
vsPath: string | undefined;
|
||||
dimensions?: ISizeCalculationResult | undefined;
|
||||
title?: string | undefined;
|
||||
caption?: string | undefined;
|
||||
alt?: string | undefined;
|
||||
mimeType?: string | undefined;
|
||||
mtime?: Date;
|
||||
ctime?: Date;
|
||||
size?: number;
|
||||
|
||||
metadata: {
|
||||
title?: string | undefined;
|
||||
caption?: string | undefined;
|
||||
alt?: string | undefined;
|
||||
[fieldName: string]: string | string[] | Date | number | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DashboardData } from './DashboardData';
|
||||
import { DataType } from './DataType';
|
||||
|
||||
export interface PanelSettings {
|
||||
git: GitSettings;
|
||||
git: GitSettings | undefined;
|
||||
seo: SEO;
|
||||
slug: Slug;
|
||||
tags: string[];
|
||||
@@ -106,7 +106,7 @@ export interface Field {
|
||||
isPreviewImage?: boolean;
|
||||
hidden?: boolean;
|
||||
taxonomyId?: string;
|
||||
default?: string;
|
||||
default?: string | number | string[] | boolean;
|
||||
fields?: Field[];
|
||||
fieldGroup?: string | string[];
|
||||
dataType?: string | string[];
|
||||
|
||||
5
src/models/i18nConfig.ts
Normal file
5
src/models/i18nConfig.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface I18nConfig {
|
||||
locale: string;
|
||||
title?: string;
|
||||
path?: string;
|
||||
}
|
||||
@@ -12,8 +12,10 @@ export * from './DataFolder';
|
||||
export * from './DataType';
|
||||
export * from './DraftField';
|
||||
export * from './Framework';
|
||||
export * from './GitRepository';
|
||||
export * from './GitSettings';
|
||||
export * from './LoadingType';
|
||||
export * from './MediaContentType';
|
||||
export * from './MediaPaths';
|
||||
export * from './Mode';
|
||||
export * from './PanelSettings';
|
||||
@@ -29,3 +31,4 @@ export * from './TaxonomyType';
|
||||
export * from './Template';
|
||||
export * from './UnmappedMedia';
|
||||
export * from './VersionInfo';
|
||||
export * from './i18nConfig';
|
||||
|
||||
@@ -83,7 +83,7 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || !localeReady) {
|
||||
if (loading && !localeReady) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
@@ -116,37 +116,44 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
|
||||
<div className={`ext_actions`}>
|
||||
<GitAction settings={settings} />
|
||||
|
||||
<CustomView metadata={metadata} />
|
||||
{!loading && (<CustomView metadata={metadata} />)}
|
||||
|
||||
<FeatureFlag features={mode?.features || [...allPanelValues]} flag={FEATURE_FLAG.panel.globalSettings}>
|
||||
<GlobalSettings settings={settings} />
|
||||
</FeatureFlag>
|
||||
|
||||
{settings && settings.seo && (
|
||||
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.seo}>
|
||||
<SeoStatus
|
||||
seo={settings.seo}
|
||||
data={metadata}
|
||||
focusElm={focusElm}
|
||||
unsetFocus={unsetFocus}
|
||||
/>
|
||||
</FeatureFlag>
|
||||
)}
|
||||
{settings && metadata && (
|
||||
{
|
||||
!loading && settings && settings.seo && (
|
||||
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.seo}>
|
||||
<SeoStatus
|
||||
seo={settings.seo}
|
||||
data={metadata}
|
||||
focusElm={focusElm}
|
||||
unsetFocus={unsetFocus}
|
||||
/>
|
||||
</FeatureFlag>
|
||||
)
|
||||
}
|
||||
|
||||
{!loading && settings && metadata && (
|
||||
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.actions}>
|
||||
<Actions metadata={metadata} settings={settings} scripts={settings.scripts} />
|
||||
</FeatureFlag>
|
||||
)}
|
||||
|
||||
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.metadata}>
|
||||
<Metadata
|
||||
settings={settings}
|
||||
metadata={metadata}
|
||||
focusElm={focusElm}
|
||||
unsetFocus={unsetFocus}
|
||||
features={mode?.features || []}
|
||||
/>
|
||||
</FeatureFlag>
|
||||
{
|
||||
!loading && (
|
||||
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.metadata}>
|
||||
<Metadata
|
||||
settings={settings}
|
||||
metadata={metadata}
|
||||
focusElm={focusElm}
|
||||
unsetFocus={unsetFocus}
|
||||
features={mode?.features || []}
|
||||
/>
|
||||
</FeatureFlag>
|
||||
)
|
||||
}
|
||||
|
||||
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.recentlyModified}>
|
||||
<FolderAndFiles data={folderAndFiles} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IActionButtonProps {
|
||||
title: JSX.Element | string;
|
||||
title: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
onClick: (e: React.SyntheticEvent<HTMLButtonElement>) => void;
|
||||
@@ -11,12 +11,13 @@ const ActionButton: React.FunctionComponent<IActionButtonProps> = ({
|
||||
className,
|
||||
onClick,
|
||||
disabled,
|
||||
title
|
||||
title,
|
||||
children
|
||||
}: React.PropsWithChildren<IActionButtonProps>) => {
|
||||
return (
|
||||
<div className={`article__action`}>
|
||||
<button onClick={onClick} className={className || ''} disabled={disabled}>
|
||||
{title}
|
||||
<div className={`article__action w-full`}>
|
||||
<button type="button" title={title} onClick={onClick} className={className || ''} disabled={disabled}>
|
||||
{children}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -28,6 +28,8 @@ export const OpenOnWebsiteAction: React.FunctionComponent<IOpenOnWebsiteActionPr
|
||||
return (
|
||||
<ActionButton
|
||||
title={l10n.t(LocalizationKey.commonOpenOnWebsite)}
|
||||
onClick={open} />
|
||||
onClick={open}>
|
||||
{l10n.t(LocalizationKey.commonOpenOnWebsite)}
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
@@ -16,7 +16,11 @@ const CustomScript: React.FunctionComponent<ICustomScriptProps> = ({
|
||||
Messenger.send(CommandToCode.runCustomScript, { title, script });
|
||||
};
|
||||
|
||||
return <ActionButton onClick={runCustomScript} title={title} />;
|
||||
return (
|
||||
<ActionButton onClick={runCustomScript} title={title}>
|
||||
{title}
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
|
||||
CustomScript.displayName = 'CustomScript';
|
||||
|
||||
@@ -11,7 +11,7 @@ import { LocalizationKey } from '../../../localization';
|
||||
|
||||
export interface IDateTimeFieldProps extends BaseFieldProps<Date | null> {
|
||||
format?: string;
|
||||
onChange: (date: string) => void;
|
||||
onChange: (date: Date) => void;
|
||||
}
|
||||
|
||||
type InputProps = JSX.IntrinsicElements['input'];
|
||||
@@ -37,11 +37,7 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({
|
||||
|
||||
const onDateChange = React.useCallback((date: Date) => {
|
||||
setDateValue(date);
|
||||
|
||||
const dateValue = DateHelper.format(date, format);
|
||||
if (dateValue) {
|
||||
onChange(dateValue);
|
||||
}
|
||||
onChange(date);
|
||||
}, [format, onChange]);
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface ITextFieldProps extends BaseFieldProps<string> {
|
||||
limit: number | undefined;
|
||||
rows?: number;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
settings: PanelSettings;
|
||||
onChange: (txtValue: string) => void;
|
||||
}
|
||||
@@ -27,6 +28,7 @@ export interface ITextFieldProps extends BaseFieldProps<string> {
|
||||
const WysiwygField = React.lazy(() => import('./WysiwygField'));
|
||||
|
||||
export const TextField: React.FunctionComponent<ITextFieldProps> = ({
|
||||
placeholder,
|
||||
singleLine,
|
||||
wysiwyg,
|
||||
limit,
|
||||
@@ -154,6 +156,7 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
|
||||
className={`metadata_field__input`}
|
||||
value={text || ''}
|
||||
onChange={(e) => onTextChange(e.currentTarget.value)}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
border
|
||||
}}
|
||||
@@ -164,6 +167,7 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
|
||||
rows={rows || 2}
|
||||
value={text || ''}
|
||||
onChange={(e) => onTextChange(e.currentTarget.value)}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
border
|
||||
}}
|
||||
|
||||
@@ -361,7 +361,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
let draftValue = parent[field.name];
|
||||
|
||||
if (!draftValue && typeof parent[field.name] === 'undefined' && field.default) {
|
||||
draftValue = field.default;
|
||||
draftValue = field.default as string;
|
||||
onSendUpdate(field.name, draftValue, parentFields);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { Messenger, messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||
import { ArrowDownTrayIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { GeneralCommands } from '../../../constants';
|
||||
@@ -8,6 +8,7 @@ import { PanelSettings } from '../../../models';
|
||||
import { ActionButton } from '../ActionButton';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { BranchIcon } from '../Icons/BranchIcon';
|
||||
|
||||
export interface IGitActionProps {
|
||||
settings: PanelSettings | undefined;
|
||||
@@ -16,21 +17,91 @@ export interface IGitActionProps {
|
||||
export const GitAction: React.FunctionComponent<IGitActionProps> = ({
|
||||
settings
|
||||
}: React.PropsWithChildren<IGitActionProps>) => {
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [commitMessage, setCommitMessage] = useState<string | undefined>(undefined);
|
||||
const [crntBanch, setCrntBranch] = useState<string | undefined>(undefined);
|
||||
const [isSyncing, setIsSyncing] = useState<"syncing" | "fetching" | "idle">("idle");
|
||||
|
||||
const pull = () => {
|
||||
Messenger.send(GeneralCommands.toVSCode.gitSync);
|
||||
const sync = () => {
|
||||
Messenger.send(GeneralCommands.toVSCode.git.sync, commitMessage);
|
||||
};
|
||||
|
||||
const messageListener = (message: MessageEvent<EventData<any>>) => {
|
||||
const { command } = message.data;
|
||||
const fetch = () => {
|
||||
Messenger.send(GeneralCommands.toVSCode.git.fetch);
|
||||
};
|
||||
|
||||
if (command === GeneralCommands.toWebview.gitSyncingStart) {
|
||||
setIsSyncing(true);
|
||||
} else if (command === GeneralCommands.toWebview.gitSyncingEnd) {
|
||||
setIsSyncing(false);
|
||||
const selectBranch = () => {
|
||||
messageHandler.send(GeneralCommands.toVSCode.git.selectBranch)
|
||||
}
|
||||
|
||||
const messageListener = React.useCallback((message: MessageEvent<EventData<any>>) => {
|
||||
const { command, payload } = message.data;
|
||||
|
||||
if (command === GeneralCommands.toWebview.git.syncingStart) {
|
||||
setIsSyncing(payload || "syncing");
|
||||
} else if (command === GeneralCommands.toWebview.git.syncingStart) {
|
||||
setIsSyncing("syncing");
|
||||
} else if (command === GeneralCommands.toWebview.git.syncingEnd) {
|
||||
setCommitMessage(undefined);
|
||||
setIsSyncing("idle");
|
||||
} else if (command === GeneralCommands.toWebview.git.branchName) {
|
||||
setCrntBranch(payload || undefined);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isCommitRequired = React.useMemo(() => {
|
||||
const requiresCommitMessage = settings?.git?.requiresCommitMessage || [];
|
||||
|
||||
if (!crntBanch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (requiresCommitMessage && requiresCommitMessage.includes(crntBanch) && !commitMessage) {
|
||||
return {
|
||||
border: '1px solid var(--vscode-inputValidation-errorBorder)'
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}, [settings?.git?.requiresCommitMessage, crntBanch, commitMessage])
|
||||
|
||||
const isCommitDisabed = React.useMemo(() => {
|
||||
const disabledBranches = settings?.git?.disabledBranches || [];
|
||||
|
||||
if (!crntBanch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (disabledBranches && disabledBranches.includes(crntBanch)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [settings?.git?.disabledBranches, crntBanch, commitMessage])
|
||||
|
||||
const isSyncDisabled = React.useMemo(() => {
|
||||
const disabledBranches = settings?.git?.disabledBranches || [];
|
||||
const requiresCommitMessage = settings?.git?.requiresCommitMessage || [];
|
||||
|
||||
if (!crntBanch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (disabledBranches && disabledBranches.includes(crntBanch)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (requiresCommitMessage && requiresCommitMessage.includes(crntBanch)) {
|
||||
return !commitMessage;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [settings?.git?.disabledBranches, settings?.git?.requiresCommitMessage, crntBanch, commitMessage]);
|
||||
|
||||
const fetchBranch = React.useCallback(() => {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.git.getBranch).then((branch) => {
|
||||
setCrntBranch(branch);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.listen(messageListener);
|
||||
@@ -38,7 +109,11 @@ export const GitAction: React.FunctionComponent<IGitActionProps> = ({
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
};
|
||||
}, []);
|
||||
}, [messageListener]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBranch();
|
||||
}, [fetchBranch]);
|
||||
|
||||
if (!settings?.git?.actions || !settings?.git.isGitRepo) {
|
||||
return null;
|
||||
@@ -46,17 +121,59 @@ export const GitAction: React.FunctionComponent<IGitActionProps> = ({
|
||||
|
||||
return (
|
||||
<div className="git_actions">
|
||||
<ActionButton
|
||||
onClick={pull}
|
||||
title={
|
||||
<h2 className='text-[11px] flex justify-between items-center mb-4'>
|
||||
<span className='uppercase'>
|
||||
{l10n.t(LocalizationKey.panelGitGitActionTitle)}
|
||||
</span>
|
||||
|
||||
<button
|
||||
className='inline-flex items-center w-auto p-0 bg-inherit text-[var(--vscode-sideBarTitle-foreground)] hover:bg-inherit hover:text-[var(--vscode-sideBarTitle-foreground-hover)]'
|
||||
title={l10n.t(LocalizationKey.panelGitGitActionBranchSelect)}
|
||||
onClick={selectBranch}>
|
||||
<BranchIcon className='w-4 h-4' aria-hidden="true" />
|
||||
<span className='ml-1'>{crntBanch}</span>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<div className='space-y-4'>
|
||||
<input
|
||||
type='text'
|
||||
className='rounded-md disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
placeholder={l10n.t(LocalizationKey.panelGitGitActionInputPlaceholder)}
|
||||
style={{
|
||||
...isCommitRequired
|
||||
}}
|
||||
value={commitMessage || ''}
|
||||
onChange={(e) => setCommitMessage(e.target.value)}
|
||||
disabled={isCommitDisabed}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
disabled={isSyncDisabled || isSyncing !== "idle"}
|
||||
onClick={sync}
|
||||
title={l10n.t(LocalizationKey.commonSync)}
|
||||
>
|
||||
<div className="git_actions__sync">
|
||||
<ArrowPathIcon className={isSyncing ? 'animate-reverse-spin' : ''} />
|
||||
<ArrowPathIcon className={isSyncing === "syncing" ? 'animate-spin' : ''} aria-hidden="true" />
|
||||
<span>
|
||||
{l10n.t(LocalizationKey.commonSync)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton
|
||||
disabled={!crntBanch || isSyncing !== "idle"}
|
||||
onClick={fetch}
|
||||
title={l10n.t(LocalizationKey.panelGitGitActionButtonFetch)}
|
||||
>
|
||||
<div className="git_actions__fetch">
|
||||
<ArrowDownTrayIcon className={isSyncing === "fetching" ? 'animate-bounce' : ''} aria-hidden="true" />
|
||||
<span>
|
||||
{l10n.t(LocalizationKey.panelGitGitActionButtonFetch)}
|
||||
</span>
|
||||
</div>
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
15
src/panelWebView/components/Icons/BranchIcon.tsx
Normal file
15
src/panelWebView/components/Icons/BranchIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IBranchIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const BranchIcon: React.FunctionComponent<IBranchIconProps> = ({
|
||||
className
|
||||
}: React.PropsWithChildren<IBranchIconProps>) => {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" className={className || ""} fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.0067 8.22168C21.0102 7.52792 20.8205 6.84689 20.4589 6.25485C20.0971 5.66281 19.5778 5.18315 18.959 4.86957C18.3401 4.556 17.6461 4.42091 16.9548 4.47941C16.2635 4.53793 15.6022 4.78773 15.0448 5.20085C14.4875 5.61397 14.0561 6.17409 13.7991 6.8185C13.5421 7.4629 13.4695 8.16613 13.5895 8.84944C13.7096 9.53274 14.0174 10.1692 14.4787 10.6874C14.94 11.2056 15.5365 11.5852 16.2012 11.7836C15.9558 12.2824 15.576 12.703 15.1047 12.9979C14.6334 13.2929 14.0892 13.4505 13.5331 13.4532H10.5437C9.43702 13.4571 8.37138 13.8727 7.55427 14.6191V7.39809C8.46159 7.21288 9.26783 6.69737 9.81668 5.95151C10.3655 5.20565 10.6178 4.28256 10.5248 3.36121C10.4317 2.43987 9.99985 1.5859 9.31292 0.964873C8.62599 0.343845 7.73295 0 6.80691 0C5.88087 0 4.98783 0.343845 4.3009 0.964873C3.61397 1.5859 3.18211 2.43987 3.08904 3.36121C2.99596 4.28256 3.24831 5.20565 3.79715 5.95151C4.34599 6.69737 5.15223 7.21288 6.05955 7.39809V16.5159C5.15393 16.6891 4.34299 17.1877 3.77969 17.9176C3.21639 18.6476 2.93968 19.5585 3.00173 20.4785C3.06379 21.3984 3.46033 22.2639 4.11656 22.9115C4.77279 23.5592 5.64335 23.9444 6.56403 23.9944C7.48472 24.0445 8.39187 23.7558 9.1144 23.183C9.83693 22.6102 10.3249 21.7928 10.4862 20.885C10.6475 19.9771 10.4712 19.0417 9.99023 18.255C9.50932 17.4683 8.75717 16.8848 7.87564 16.6145C8.12152 16.1162 8.50142 15.6963 8.97272 15.4019C9.44401 15.1074 9.98803 14.9503 10.5437 14.9479H13.5331C14.4658 14.9436 15.3739 14.6486 16.1311 14.1039C16.8882 13.5592 17.4566 12.792 17.7572 11.9091C18.6531 11.7914 19.476 11.3528 20.0735 10.6748C20.671 9.9968 21.0025 9.12533 21.0067 8.22168ZM4.56483 3.73752C4.56483 3.29408 4.69633 2.8606 4.94269 2.4919C5.18906 2.12319 5.53922 1.83581 5.9489 1.66611C6.3586 1.49642 6.8094 1.45202 7.24432 1.53854C7.67924 1.62504 8.07874 1.83857 8.3923 2.15214C8.70586 2.4657 8.9194 2.8652 9.00591 3.30012C9.09241 3.73504 9.04802 4.18585 8.87832 4.59553C8.70862 5.00521 8.42125 5.35539 8.05254 5.60175C7.68383 5.84811 7.25035 5.9796 6.80691 5.9796C6.21227 5.9796 5.642 5.74339 5.22152 5.32291C4.80105 4.90245 4.56483 4.33216 4.56483 3.73752ZM9.04899 20.1794C9.04899 20.6229 8.91749 21.0563 8.67113 21.425C8.42476 21.7937 8.0746 22.0811 7.66492 22.2508C7.25523 22.4205 6.80442 22.4649 6.36951 22.3784C5.93458 22.292 5.53509 22.0784 5.22152 21.7648C4.90796 21.4512 4.69443 21.0517 4.60791 20.6169C4.52141 20.1819 4.5658 19.7311 4.7355 19.3214C4.9052 18.9117 5.19258 18.5615 5.56128 18.3152C5.92999 18.0689 6.36347 17.9373 6.80691 17.9373C7.40155 17.9373 7.97183 18.1736 8.3923 18.594C8.81277 19.0145 9.04899 19.5848 9.04899 20.1794ZM17.2699 10.4638C16.8265 10.4638 16.393 10.3322 16.0243 10.0859C15.6556 9.83954 15.3683 9.48937 15.1986 9.07969C15.0289 8.67 14.9844 8.2192 15.0709 7.78427C15.1574 7.34935 15.3709 6.94985 15.6845 6.63629C15.9981 6.32273 16.3976 6.10919 16.8325 6.02268C17.2674 5.93617 17.7183 5.98058 18.1279 6.15027C18.5377 6.31997 18.8878 6.60734 19.1341 6.97605C19.3805 7.34476 19.512 7.77823 19.512 8.22168C19.512 8.81632 19.2757 9.3866 18.8553 9.80706C18.4348 10.2275 17.8645 10.4638 17.2699 10.4638Z" fill="currentcolor" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { PencilIcon, ChevronUpDownIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { SortableHandle, SortableElement } from 'react-sortable-hoc';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
|
||||
export interface IJsonFieldRecordProps {
|
||||
id: number;
|
||||
index: number;
|
||||
@@ -30,20 +33,20 @@ export const JsonFieldRecord = SortableElement(
|
||||
|
||||
<div>
|
||||
<button
|
||||
title="Edit record"
|
||||
title={l10n.t(LocalizationKey.commonEdit)}
|
||||
className="json_data__list__button json_data__list__button_edit"
|
||||
onClick={() => onEdit(id)}
|
||||
>
|
||||
<PencilIcon className="json_data__list__button_icon" />
|
||||
<span className="sr-only">Edit</span>
|
||||
<span className="sr-only">{l10n.t(LocalizationKey.commonEdit)}</span>
|
||||
</button>
|
||||
<button
|
||||
title="Delete record"
|
||||
title={l10n.t(LocalizationKey.commonDelete)}
|
||||
className="json_data__list__button json_data__list__button_delete"
|
||||
onClick={() => onDelete(id)}
|
||||
>
|
||||
<TrashIcon className="json_data__list__button_icon" />
|
||||
<span className="sr-only">Delete</span>
|
||||
<span className="sr-only">{l10n.t(LocalizationKey.commonDelete)}</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -3,6 +3,9 @@ import * as React from 'react';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { JsonFieldRecord } from '.';
|
||||
import { SortableContainer, SortEnd } from 'react-sortable-hoc';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
|
||||
export interface IJsonFieldRecordsProps {
|
||||
records: any[];
|
||||
selectedIndex: number | null;
|
||||
@@ -37,7 +40,7 @@ export const JsonFieldRecords = ({
|
||||
<span style={{ lineHeight: '16px' }}>Records</span>
|
||||
</div>
|
||||
|
||||
<button title="Add new record" className="json_data__list__button" onClick={onAdd}>
|
||||
<button title={l10n.t(LocalizationKey.commonAdd)} className="json_data__list__button" onClick={onAdd}>
|
||||
<PlusIcon style={{ width: '16px', height: '16px' }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,11 @@ const Preview: React.FunctionComponent<IPreviewProps> = (_: React.PropsWithChild
|
||||
Messenger.send(CommandToCode.openPreview);
|
||||
};
|
||||
|
||||
return <ActionButton onClick={open} title={l10n.t(LocalizationKey.panelPreviewTitle)} />;
|
||||
return (
|
||||
<ActionButton onClick={open} title={l10n.t(LocalizationKey.panelPreviewTitle)}>
|
||||
{l10n.t(LocalizationKey.panelPreviewTitle)}
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
|
||||
Preview.displayName = 'Preview';
|
||||
|
||||
@@ -23,7 +23,9 @@ const PublishAction: React.FunctionComponent<IPublishActionProps> = (
|
||||
onClick={publish}
|
||||
className={`${draft ? '' : 'secondary'}`}
|
||||
title={draft ? l10n.t(LocalizationKey.panelPublishActionPublish) : l10n.t(LocalizationKey.panelPublishActionUnpublish)}
|
||||
/>
|
||||
>
|
||||
{draft ? l10n.t(LocalizationKey.panelPublishActionPublish) : l10n.t(LocalizationKey.panelPublishActionUnpublish)}
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ const SlugAction: React.FunctionComponent<
|
||||
Messenger.send(CommandToCode.updateSlug);
|
||||
};
|
||||
|
||||
return <ActionButton onClick={optimize} title={l10n.t(LocalizationKey.panelSlugActionTitle)} />;
|
||||
return (
|
||||
<ActionButton onClick={optimize} title={l10n.t(LocalizationKey.panelSlugActionTitle)}>
|
||||
{l10n.t(LocalizationKey.panelSlugActionTitle)}
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
|
||||
SlugAction.displayName = 'SlugAction';
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
/* Animations */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,12 +976,14 @@ vscode-divider {
|
||||
}
|
||||
|
||||
/* Git actions */
|
||||
.git_actions__fetch,
|
||||
.git_actions__sync {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.git_actions__fetch svg,
|
||||
.git_actions__sync svg {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { i18n } from './../commands/i18n';
|
||||
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
|
||||
import { parseWinPath } from './../helpers/parseWinPath';
|
||||
import { dirname, extname, join } from 'path';
|
||||
@@ -76,6 +77,7 @@ export class PagesParser {
|
||||
* Parse all pages in the workspace
|
||||
*/
|
||||
public static async parsePages() {
|
||||
i18n.clearFiles();
|
||||
const ext = Extension.getInstance();
|
||||
|
||||
// Update the dashboard with the fresh data
|
||||
@@ -209,6 +211,10 @@ export class PagesParser {
|
||||
escapedDescription = '<invalid title>';
|
||||
}
|
||||
|
||||
const isDefaultLanguage = await i18n.isDefaultLanguage(filePath);
|
||||
const locale = await i18n.getLocale(filePath);
|
||||
const translations = await i18n.getTranslations(filePath);
|
||||
|
||||
const page: Page = {
|
||||
...article.data,
|
||||
// Cache properties
|
||||
@@ -230,6 +236,10 @@ export class PagesParser {
|
||||
fmContentType: contentType.name || DEFAULT_CONTENT_TYPE_NAME,
|
||||
fmBody: article?.content || '',
|
||||
fmDateFormat: dateFormat,
|
||||
// i18n properties
|
||||
fmDefaultLocale: isDefaultLanguage,
|
||||
fmLocale: locale,
|
||||
fmTranslations: translations,
|
||||
// Make sure these are always set
|
||||
title: escapedTitle,
|
||||
description: escapedDescription,
|
||||
|
||||
6
src/utils/cn.ts
Normal file
6
src/utils/cn.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user