Compare commits

...

198 Commits

Author SHA1 Message Date
Elio Struyf
511fd48081 Merge pull request #224 from estruyf/dev 2022-01-10 13:01:03 +01:00
Elio Struyf
0039fc1555 Release date added 2022-01-10 13:00:32 +01:00
Elio Struyf
98044187cd Update changelog 2022-01-07 13:03:26 +01:00
Elio Struyf
a6dcc1ea79 Merge pull request #222 from farmerau/onWillSaveTextDocument 2022-01-07 12:55:34 +01:00
Elio Struyf
32a686227e #221 - Logic to remove new line by gray matter 2022-01-07 12:54:39 +01:00
Austin Farmer
faa74132e5 Adjust Automatic Date Updates to Run on Save
Prefers `onWillSaveTextDocument` over `onDidChangeTextDocument` for the triggering event.
2022-01-06 15:44:36 -05:00
Elio Struyf
3a847f7e42 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2022-01-05 18:49:40 +01:00
Elio Struyf
66c978891e updated changelog 2022-01-05 18:49:18 +01:00
Elio Struyf
2f31230e07 Merge pull request #220 from farmerau/markdown-file-heuristics-mdx 2022-01-05 18:40:01 +01:00
Austin Farmer
c4225c0011 Support detection by file extension 2022-01-05 11:38:32 -05:00
Elio Struyf
4edc7a0280 Updated filetype 2022-01-05 10:17:34 +01:00
Elio Struyf
a60fe5204b Updated config 2022-01-04 11:57:16 +01:00
Elio Struyf
bb980b4afe #218 - Added MDX support for template and content creation 2022-01-04 11:51:11 +01:00
Elio Struyf
504658d87a 5.10.0 2022-01-04 11:24:40 +01:00
Elio Struyf
feff69d969 Merge pull request #217 from estruyf/dev 2022-01-01 20:21:23 +01:00
Elio Struyf
a34d77242a Updated entry 2022-01-01 20:20:57 +01:00
Elio Struyf
1b24c1277d Merge pull request #216 from estruyf/dev 2022-01-01 20:15:04 +01:00
Elio Struyf
e6750205be 🎇 New Year release 🎆 2022-01-01 20:14:15 +01:00
Elio Struyf
1da8bf3f8b To lowercase links 2021-12-29 19:14:10 +01:00
Elio Struyf
90c60b6a40 Merge branch 'LuiseFreese-main' into dev 2021-12-29 19:12:35 +01:00
Luise Freese
ee79f89c7f fixes casing issues that led to links that only referred to settings site but not to the specific heading 2021-12-29 11:17:11 +01:00
Elio Struyf
0668d48fd5 updated changelog 2021-12-28 21:20:16 +01:00
Elio Struyf
d046f73d16 #214 - Open file after creation fix 2021-12-28 20:52:42 +01:00
Elio Struyf
f144d713d1 added edit quick action 2021-12-28 10:45:33 +01:00
Elio Struyf
d31c403bdc Update icon 2021-12-28 10:43:36 +01:00
Elio Struyf
35a0327387 Added quick actions + some styling fixes 2021-12-28 10:35:46 +01:00
Elio Struyf
9b39649bde Update description 2021-12-27 16:29:37 +01:00
Elio Struyf
3d857463f0 Updated extension description 2021-12-27 16:25:55 +01:00
Elio Struyf
0428642a2f Update changelog 2021-12-27 16:22:26 +01:00
Elio Struyf
5182a9ae1a Fix spinner overlapping the global navigation 2021-12-27 16:22:01 +01:00
Elio Struyf
ab3686b3b5 #199 - Search media files 2021-12-27 15:56:12 +01:00
Elio Struyf
c5b7b7845d #212 - Added deleted folder watcher 2021-12-26 20:29:44 +01:00
Elio Struyf
2f13c335ed #212 - Added folder watchers 2021-12-26 20:24:23 +01:00
Elio Struyf
f219ac721f Updated navigation header 2021-12-26 19:13:42 +01:00
Elio Struyf
0149885289 Item borders 2021-12-26 18:56:24 +01:00
Elio Struyf
cb80a10de2 #213 - Media folder design 2021-12-26 17:19:31 +01:00
Elio Struyf
092eb0fd2a #211 - Replace text selection on media insert 2021-12-26 12:11:38 +01:00
Elio Struyf
24f79d9d3f #210 - Media file extension fix 2021-12-26 11:52:45 +01:00
Elio Struyf
d667b19716 5.9.0 2021-12-26 11:40:28 +01:00
Elio Struyf
4d38a0881a Merge pull request #209 from estruyf/dev 2021-12-21 15:45:06 +01:00
Elio Struyf
1478d0bd53 🎄 release 🎄 2021-12-21 15:39:43 +01:00
Elio Struyf
00a9e59bc2 #208 - Fix collapse section issue 2021-12-21 15:25:27 +01:00
Elio Struyf
2b2843234e Updated changelog 2021-12-21 11:13:46 +01:00
Elio Struyf
71449aa5cc Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2021-12-21 11:13:34 +01:00
Elio Struyf
87f293a3f6 added charset 2021-12-21 11:13:30 +01:00
Elio Struyf
6da3ddb5dd #206 - Removal of the front matter parsing error notification 2021-12-20 20:38:56 +01:00
Elio Struyf
87e80ccfe9 Update readme 2021-12-18 21:15:57 +01:00
Elio Struyf
af9865d91b Refresh the pages on content creation 2021-12-17 19:21:59 +01:00
Elio Struyf
e576b6e8a4 #207 - fix quick picks 2021-12-17 19:21:41 +01:00
Elio Struyf
c9f4c8f94e #206 - Added front matter diagnostic parsing 2021-12-17 19:04:41 +01:00
Elio Struyf
6ef63da973 Add sentry ignore rule 2021-12-16 14:02:57 +01:00
Elio Struyf
2d85b0a554 Updates for Webpack 5 support + HMR during dev 2021-12-16 13:31:30 +01:00
Elio Struyf
41cbcc4c46 Retain context on dashboard when hidden 2021-12-15 15:37:27 +01:00
Elio Struyf
6c94880497 Make output bundles smaller by smarter importing icons 2021-12-15 14:47:49 +01:00
Elio Struyf
1d75c6b6b8 Updated vs code engine 2021-12-15 14:22:47 +01:00
Elio Struyf
338bc022f9 Updated node version 2021-12-15 14:18:56 +01:00
Elio Struyf
2451730311 Updated workflow 2021-12-15 14:16:07 +01:00
Elio Struyf
22cdaabe28 Update changelog 2021-12-15 14:12:26 +01:00
Elio Struyf
5a69240178 #205 - Define a logging level setting 2021-12-15 14:10:58 +01:00
Elio Struyf
b81ef077f6 Merge branch 'dev-refactor' into dev 2021-12-15 13:51:01 +01:00
Elio Struyf
cb72b53653 Refactoring to message listeners 2021-12-15 13:50:40 +01:00
Elio Struyf
050a513b48 Refactoring 2021-12-15 11:16:39 +01:00
Elio Struyf
481d1f56e4 5.8.0 2021-12-15 08:29:41 +01:00
Elio Struyf
229238703a Updated vscode types 2021-12-15 08:29:35 +01:00
Elio Struyf
074de212ce Merge pull request #203 from estruyf/dev 2021-12-07 10:11:07 +01:00
Elio Struyf
98e4490318 updated changelog 2021-12-07 10:06:45 +01:00
Elio Struyf
c79dc3c41f Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2021-12-06 17:32:08 +01:00
Elio Struyf
b45abe88ac Updated changelog 2021-12-06 17:32:04 +01:00
Elio Struyf
2bac596e3d #200 - media file sorting fix 2021-12-05 13:11:22 +01:00
Elio Struyf
15a6ea8d8d #194 - Optimizations for the markup options 2021-12-04 13:55:52 +01:00
Elio Struyf
72d6f8263a Updated changelog 2021-12-04 09:18:21 +01:00
Elio Struyf
7549b3d989 #202 - Fix checkbox label color for light themes 2021-12-04 09:18:04 +01:00
Elio Struyf
ca628b37de #201 - Fix filename overflow 2021-12-03 19:15:20 +01:00
Elio Struyf
db120b5c45 #194 - ordered/unordered/task lists added 2021-12-03 19:06:27 +01:00
Elio Struyf
2973f5d27b #194 - Added heading and blockquote support 2021-12-03 14:11:18 +01:00
Elio Struyf
075c7ad350 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2021-11-30 15:09:05 +01:00
Elio Struyf
23625ed8fa Merge branch 'issue/194' into dev 2021-11-30 15:06:05 +01:00
Elio Struyf
9bd06909c8 #194 - WYSIWYG controls implementation 2021-11-30 15:05:28 +01:00
Elio Struyf
1b45c2af13 Update changelog 2021-11-26 08:42:08 +01:00
Elio Struyf
b4bbf6c6fb #191 - Fix beta settings 2021-11-26 08:41:43 +01:00
Elio Struyf
a5b8d9668d Merge branch 'main' into dev 2021-11-25 15:23:43 +01:00
Elio Struyf
490210da00 Updated changelog 2021-11-25 15:22:56 +01:00
Elio Struyf
6878d440b1 Merge branch 'main' into dev 2021-11-25 15:21:06 +01:00
Elio Struyf
81fa0a7d0f #188 - support for markdown file extension added 2021-11-25 15:20:19 +01:00
Elio Struyf
b7d4e547a1 Total nr of files in workspace 2021-11-25 12:59:49 +01:00
Elio Struyf
2b2e256b8f #190 - More diagnostic values 2021-11-25 12:12:32 +01:00
Elio Struyf
7a7b31c2ae #190 - First implementation of the diagnostic output 2021-11-25 11:00:47 +01:00
Elio Struyf
dea5eb1053 5.7.0 2021-11-25 10:46:35 +01:00
Elio Struyf
df3144ea73 Remove unused reference 2021-11-25 10:46:30 +01:00
Elio Struyf
a170cc3ad9 Updated sponsors 2021-11-24 08:36:56 +01:00
Elio Struyf
35b3813022 Merge pull request #184 from estruyf/dev 2021-11-23 20:02:10 +01:00
Elio Struyf
f0472fe89b Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2021-11-23 20:00:04 +01:00
Elio Struyf
7bdb2fa025 Updated changelog for new 5.6.0 release 2021-11-23 19:59:11 +01:00
Elio Struyf
5ba7ad449a Updated changelog 2021-11-23 19:56:13 +01:00
Elio Struyf
145441d55e Updated package sorting 2021-11-22 17:12:29 +01:00
Elio Struyf
62ff3419c1 #181 - Support for custom taxonomy fields added 2021-11-22 16:43:30 +01:00
Elio Struyf
34aee134d6 #183 - Updated changelog 2021-11-22 08:09:20 +01:00
Elio Struyf
5899120d87 Updated sort setting 2021-11-21 18:58:39 +01:00
Elio Struyf
816a2fefe7 #182 - Support default sort option 2021-11-21 17:56:49 +01:00
Elio Struyf
1f64e59917 Fix sorting object 2021-11-21 17:28:02 +01:00
Elio Struyf
5e54334fb9 #97 - Support for folder scripts added 2021-11-21 12:07:24 +01:00
Elio Struyf
d354af306f Update changelog 2021-11-21 11:35:16 +01:00
Elio Struyf
045cece0ce #97 - Custom scripts to support media management 2021-11-21 11:32:56 +01:00
Elio Struyf
506012200f First steps to allow custom scripts for media files 2021-11-20 16:59:41 +01:00
Elio Struyf
4ad2f0d495 Remove image compress icon 2021-11-19 16:36:32 +01:00
Elio Struyf
b00e3cfd4b Image compression icon test 2021-11-19 16:02:56 +01:00
Elio Struyf
bb05489872 Fix postinstall 2021-11-19 15:49:12 +01:00
Elio Struyf
ede4d417bd Various updates 2021-11-19 15:45:08 +01:00
Elio Struyf
928072fa27 #178 - Type fixes 2021-11-18 19:55:56 +01:00
Elio Struyf
6a313fcc8a #178 - Making it faster 2021-11-18 19:53:37 +01:00
Elio Struyf
725f7f4915 #178 - Revert sorting implementation 2021-11-18 17:23:05 +01:00
Elio Struyf
b9cb0ea16d #180 - Filename placeholder added 2021-11-18 16:42:34 +01:00
Elio
61b80795a4 #178 - Windows support with PowerShell added 2021-11-18 16:24:45 +01:00
Elio Struyf
8daaa23774 #178 - macOS and Linux support for date retrieval 2021-11-18 12:56:23 +01:00
Elio Struyf
8a0d308ceb #178 - Sorting for media files 2021-11-17 14:18:04 +01:00
Elio Struyf
e6a7a9aae7 #179 - New open dashboard icon 2021-11-17 10:03:19 +01:00
Elio Struyf
f3943bd846 5.6.0 2021-11-17 09:07:47 +01:00
Elio Struyf
d2ae94df34 Merge pull request #177 from estruyf/dev 2021-11-15 10:42:15 +01:00
Elio Struyf
f799613b1a Update changelog 2021-11-15 10:38:46 +01:00
Elio Struyf
3f057a01d8 Removed char 2021-11-12 15:36:51 +01:00
Elio Struyf
3ad5136735 #173 - Persistent sorting 2021-11-12 12:11:46 +01:00
Elio Struyf
e201ce6f83 updated changelog 2021-11-12 10:55:26 +01:00
Elio Struyf
383a3a7d4c Updated changelog 2021-11-10 15:19:12 +01:00
Elio Struyf
8261f1de1b #173 - Allow to specify your own sorting 2021-11-10 15:18:35 +01:00
Elio Struyf
5b38e6fa56 Updated changelog 2021-11-10 11:55:33 +01:00
Elio Struyf
717f34bc85 #174 - Enhancement for excluding sub-directories 2021-11-10 11:54:39 +01:00
Elio Struyf
47fb2a90a9 5.5.0 2021-11-10 11:16:24 +01:00
Elio Struyf
85a7221895 Updated flows 2021-11-09 10:02:38 +01:00
Elio Struyf
9618a89528 Merge pull request #169 from estruyf/dev 2021-11-05 09:25:09 +01:00
Elio Struyf
14f0af2754 Updated changelog 2021-11-05 09:24:26 +01:00
Elio Struyf
ebe248670d 5.4.0 2021-11-03 12:00:56 +01:00
Elio Struyf
511960c4a9 #167 - Allow to set a preview path per content type 2021-11-03 12:00:49 +01:00
Elio Struyf
31fd1f93ce Url joining 2021-11-02 16:32:36 +01:00
Elio Struyf
6625b69170 #166 - Filename and folder logic for slug 2021-11-02 13:48:52 +01:00
Elio Struyf
9e8533fbb8 Updated changelog 2021-11-02 11:58:43 +01:00
Elio Struyf
9c9cbb7dcb #166 - Add preview button to panel when no markdown file is opened 2021-11-02 11:56:40 +01:00
Elio Struyf
079a13e161 Merge pull request #164 from estruyf/dev
Merge for 5.3.1
2021-10-29 10:25:17 +02:00
Elio Struyf
69c1e587d0 5.3.1 2021-10-29 10:18:31 +02:00
Elio Struyf
3996252531 #163 - Set workspace state instead of global 2021-10-29 10:18:20 +02:00
Elio Struyf
4fddda65e6 Merge pull request #162 from estruyf/dev
5.3.0 merge
2021-10-28 15:33:06 +02:00
Elio Struyf
5916344092 added release notes 2021-10-28 12:00:24 +02:00
Elio Struyf
b96722dd69 #158 - Update boolean field check 2021-10-27 11:42:57 +02:00
Elio Struyf
263ccab311 5.3.0 2021-10-26 15:59:45 +02:00
Elio Struyf
3571af82c7 Updated readme 2021-10-26 15:59:31 +02:00
Elio Struyf
c60520c0ff Fix replace in action button 2021-10-26 15:28:01 +02:00
Elio Struyf
b473431eae #159 - SEO enhancements 2021-10-26 14:42:01 +02:00
Elio Struyf
cbf434f741 Updated changelog 2021-10-25 12:54:19 +02:00
Elio Struyf
04c401207f #158 - New draft field setting + choice implementation 2021-10-25 12:53:04 +02:00
Elio Struyf
7291e6aac6 Fix tag replacement 2021-10-25 10:24:08 +02:00
Elio Struyf
a7aab96f0e Fix time formatting 2021-10-25 10:11:31 +02:00
Elio Struyf
f500749644 Fix slug punctuation 2021-10-20 15:21:27 +02:00
Elio Struyf
47e59bc54c Merge pull request #157 from estruyf/dev
Merge for v5.2.0 release
2021-10-19 15:41:12 +02:00
Elio Struyf
8902e25021 Release 5.2.0 2021-10-19 15:40:17 +02:00
Elio Struyf
33093e1eb4 Change useEffect order 2021-10-19 14:56:49 +02:00
Elio Struyf
d36178c44f Added component display names for better error reporting 2021-10-19 14:46:47 +02:00
Elio Struyf
15b09ccc75 #156 - Fix for media files in new folder 2021-10-19 11:04:05 +02:00
Elio Struyf
dffa6c87a0 Fix for rendering less hooks 2021-10-19 08:45:32 +02:00
Elio Struyf
c4a1caee09 Remove console 2021-10-18 14:33:32 +02:00
Elio Struyf
1d9f07b86d #155 - Fallback image 2021-10-18 14:29:54 +02:00
Elio Struyf
a794a95bb8 #154 - Implementation of the bulk script execution 2021-10-18 10:13:34 +02:00
Elio Struyf
40a56f6057 Fix SEO keyword check 2021-10-17 19:51:41 +02:00
Elio Struyf
82353f7b64 validate date 2021-10-17 15:31:19 +02:00
Elio Struyf
82a22da90a check the value of remove puntuation 2021-10-17 15:28:05 +02:00
Elio Struyf
380e40ea05 update changelog 2021-10-17 15:25:22 +02:00
Elio Struyf
2bedb23341 #153 - Support old date-fns format 2021-10-17 15:24:26 +02:00
Elio Struyf
1110b76364 Update changelog 2021-10-15 16:14:42 +02:00
Elio Struyf
d19e632f80 #151 #152 - Framework detection + preset public folder 2021-10-15 16:11:59 +02:00
Elio Struyf
4e040b5f7a 5.2.0 2021-10-15 14:02:08 +02:00
Elio Struyf
7a2a0934c2 Merge pull request #150 from estruyf/dev
Merge for 5.1.1
2021-10-14 16:24:29 +02:00
Elio Struyf
d3eb7b223c 5.1.1 2021-10-14 16:23:51 +02:00
Elio Struyf
f74eec954f #149 - Fix keywords 2021-10-14 16:23:28 +02:00
Elio Struyf
864c4e7aa6 Merge pull request #148 from estruyf/dev
Merge for v5.1.0
2021-10-13 11:34:29 +02:00
Elio Struyf
5667906caf Update changelog for release 2021-10-13 10:46:06 +02:00
Elio Struyf
2fe7c524e7 #147 - Error boundary added for metadata fields 2021-10-13 09:05:32 +02:00
Elio Struyf
5cc83526ad Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2021-10-13 08:27:48 +02:00
Elio Struyf
76b5e99a08 File check 2021-10-13 08:27:18 +02:00
Elio Struyf
7d5505d421 Updated changelog 2021-10-12 20:34:15 +02:00
Elio Struyf
d97a11f8b5 #146 - Date parsing logic added with fallbacks 2021-10-12 20:34:06 +02:00
Elio Struyf
0590cec684 Add version and environment details 2021-10-12 19:49:34 +02:00
Elio Struyf
b4cdc4feb9 Version fixes 2021-10-12 15:52:54 +02:00
Elio Struyf
986fd95524 #145 - Moved folder registration settings 2021-10-12 15:22:43 +02:00
Elio Struyf
f51fec5fb9 Fix issue in sorting of taxonomy picker 2021-10-12 10:50:42 +02:00
Elio Struyf
8198ce2af3 Added VSCode webcomponents 2021-10-12 10:23:55 +02:00
Elio Struyf
defffc4c8e Remove unused settings 2021-10-12 09:12:18 +02:00
Elio Struyf
8f47cbfb0b #141 - Merge content creation logic to ArticleHelper 2021-10-12 09:11:47 +02:00
Elio Struyf
0be91c17d0 #141 - Added support for page bundles with templates 2021-10-11 21:40:05 +02:00
Elio Struyf
fae7ab8417 Date to string fix 2021-10-11 17:49:45 +02:00
Elio Struyf
df239f2cc0 #144 - Pass a default value when null 2021-10-11 11:10:47 +02:00
Elio Struyf
70099dc97f #144 - Fix date value issue 2021-10-11 11:08:45 +02:00
Elio Struyf
c11be0e3ec #143 - Fix for recent files unique values 2021-10-11 10:55:58 +02:00
Elio Struyf
f8b7870180 Updated changlog 2021-10-11 10:50:28 +02:00
Elio Struyf
c58a5c62d9 #142 - Fix for unknown tags 2021-10-11 10:49:46 +02:00
Elio Struyf
ce92444bf2 Updated dashboard icon 2021-10-11 10:39:50 +02:00
Elio Struyf
b2709ebffd #141 - Support opening of the page bundle folder 2021-10-11 10:21:40 +02:00
Elio Struyf
2b20cf9d24 Updated changelog 2021-10-11 09:21:56 +02:00
Elio Struyf
f4a499ad0f 5.1.0 2021-10-11 09:20:48 +02:00
Elio Struyf
4494b158c0 #141 - Page bundle functionality 2021-10-11 09:20:42 +02:00
Elio Struyf
3416e55264 Fix rending more hooks 2021-10-08 15:30:56 +02:00
195 changed files with 19265 additions and 8650 deletions

View File

@@ -11,9 +11,9 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
registry-url: https://registry.npmjs.org/
- name: Install the dependencies
@@ -25,3 +25,6 @@ jobs:
- name: Publish
run: npx vsce publish -p ${{ secrets.VSCE_PAT }} --baseImagesUrl https://raw.githubusercontent.com/estruyf/vscode-front-matter/dev
- name: Publish to open-vsx.org
run: npx ovsx publish -p ${{ secrets.OPEN_VSX_PAT }}

View File

@@ -12,9 +12,9 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
registry-url: https://registry.npmjs.org/
- name: Install the dependencies
@@ -26,3 +26,5 @@ jobs:
- name: Publish
run: npx vsce publish -p ${{ secrets.VSCE_PAT }}
- name: Publish to open-vsx.org
run: npx ovsx publish -p ${{ secrets.OPEN_VSX_PAT }}

View File

@@ -17,4 +17,9 @@ postcss.config.js
.templates
.github
scripts
.all-contributorsrc
.all-contributorsrc
assets/v2.*
assets/v3.*
assets/v4.*
assets/sponsors
dist/*.html

View File

@@ -1,5 +1,149 @@
# Change Log
## [5.10.0] - 2022-01-10
### 🎨 Enhancements
- [#218](https://github.com/estruyf/vscode-front-matter/issues/218): Add support for creating `mdx` files from templates and content types. This introduced a new setting: `frontMatter.content.defaultFileType`.
- [#220](https://github.com/estruyf/vscode-front-matter/issues/220): Add support DateTime updates in `mdx` files when the `mdx extension` is not installed.
### 🐞 Fixes
- [#221](https://github.com/estruyf/vscode-front-matter/issues/221): Automatic DateTime switch from on text change to on save to prevent multiple updates.
## [5.9.0] - 2022-01-01 - 🎇🎆
### 🎨 Enhancements
- Fixing the spinner which overlaps the global navigation bar
- Quick actions added for media files (edit, delete, insert markdown, insert snippet)
- [#199](https://github.com/estruyf/vscode-front-matter/issues/199): Search media files in the currently selected folder
- [#211](https://github.com/estruyf/vscode-front-matter/issues/211): Replace text selection on media inserts
- [#212](https://github.com/estruyf/vscode-front-matter/issues/212): Create folder watchers for content folders. When new content gets created, the dashboard updates.
- [#213](https://github.com/estruyf/vscode-front-matter/issues/213): New media folder overview design
### 🐞 Fixes
- [#210](https://github.com/estruyf/vscode-front-matter/issues/210): Fix for adding media files with uppercase file extensions
- [#214](https://github.com/estruyf/vscode-front-matter/issues/214): Fix for opening markdown file after creating it for the specified content type
## [5.8.0] - 2021-12-21 - 🎄
### 🎨 Enhancements
- Refactoring of the WebView logic to new message handlers
- Optimized the `getMedia` call from the webview
- Keep the dashboard its context when switching tabs
- [#205](https://github.com/estruyf/vscode-front-matter/issues/205): Define a logging level setting
- [#206](https://github.com/estruyf/vscode-front-matter/issues/206): Add front matter issues to the diagnostic tab
### 🐞 Fixes
- [#207](https://github.com/estruyf/vscode-front-matter/issues/207): Fix the quick picks for content creation
- [#208](https://github.com/estruyf/vscode-front-matter/issues/208): Fix for the collapse sections action so that it is not available everywhere, but only on the Front Matter panel
## [5.7.0] - 2021-12-07 - [Release Notes](https://beta.frontmatter.codes/updates/v5.7.0)
### 🎨 Enhancements
- [#188](https://github.com/estruyf/vscode-front-matter/issues/188): Support for `.markdown` files added to the dashboard
- [#190](https://github.com/estruyf/vscode-front-matter/issues/190): Diagnostic output for the extension
- [#194](https://github.com/estruyf/vscode-front-matter/issues/194): WYSIWYG controls added for markdown files + configuration to enable/disable the functionality
### 🐞 Fixes
- [#191](https://github.com/estruyf/vscode-front-matter/issues/191): Fix beta settings page
- [#200](https://github.com/estruyf/vscode-front-matter/issues/200): Fix last modified date sorting for media files
- [#201](https://github.com/estruyf/vscode-front-matter/issues/201): Fix overflow issue with the media filename
- [#202](https://github.com/estruyf/vscode-front-matter/issues/202): Fix checkbox label color for light themes
## [5.6.0] - 2021-11-23
### 🎨 Enhancements
- Updated camera icon from VS Code to media icon
- Updated the media card actions to show it within a menu. This will give a better experience with custom scripts.
- [#97](https://github.com/estruyf/vscode-front-matter/issues/97): Custom Script support for media files and folders
- [#178](https://github.com/estruyf/vscode-front-matter/issues/178): Sorting added to the media dashboard
- [#179](https://github.com/estruyf/vscode-front-matter/issues/179): Updated the `open dashboard` icon to make it easier to spot it
- [#180](https://github.com/estruyf/vscode-front-matter/issues/180): Added `{filename}` as placeholder for media snippets
- [#181](https://github.com/estruyf/vscode-front-matter/issues/181): Support for custom taxonomy fields added
### 🐞 Fixes
- [#183](https://github.com/estruyf/vscode-front-matter/issues/183): Fix type error on the `frontMatter.content.sorting` setting
## [5.5.0] - 2021-11-15
As from this version onwards, the extension will be published to [open-vsx.org](https://open-vsx.org/).
### 🎨 Enhancements
- [#173](https://github.com/estruyf/vscode-front-matter/issues/173): Allow to specify your own sorting for the content dashboard
- [#174](https://github.com/estruyf/vscode-front-matter/issues/174): Added option to exclude sub-directories from page/markdown content retrieval
## [5.4.0] - 2021-11-05
### 🎨 Enhancements
- [#166](https://github.com/estruyf/vscode-front-matter/issues/166): Added preview button to the panel base view
- [#167](https://github.com/estruyf/vscode-front-matter/issues/167): Allow to set the preview path per content type
## [5.3.1] - 2021-10-29
### 🐞 Fixes
- [#163](https://github.com/estruyf/vscode-front-matter/issues/163): Setting workspace state instead of global state for the media view
## [5.3.0] - 2021-10-28 - [Release Notes](https://beta.frontmatter.codes/updates/v5.3.0)
### 🎨 Enhancements
- [#158](https://github.com/estruyf/vscode-front-matter/issues/158): Add support for non-boolean draft/publish status fields
- [#159](https://github.com/estruyf/vscode-front-matter/issues/159): Enhancements to SEO checks: Slug check, keyword details, more article information
### 🐞 Fixes
- Value check when generating slug from title
- Fix for date time formatting with `DD` and `YYYY` tokens
- Fix in tag space replacing when object is passed
## [5.2.0] - 2021-10-19
### 🎨 Enhancements
- [#151](https://github.com/estruyf/vscode-front-matter/issues/151): Detect which site-generator or framework is used
- [#152](https://github.com/estruyf/vscode-front-matter/issues/152): Automatically set setting based on the used site-generator or framework
- [#154](https://github.com/estruyf/vscode-front-matter/issues/154): Bulk script support added
- [#155](https://github.com/estruyf/vscode-front-matter/issues/155): Fallback image added for the images shown in the editor panel
### 🐞 Fixes
- [#153](https://github.com/estruyf/vscode-front-matter/issues/153): Support old date formatting for date-fns
- [#156](https://github.com/estruyf/vscode-front-matter/issues/156): Fix for uploading media files into a new folder
## [5.1.1] - 2021-10-14
### 🐞 Fixes
- [#149](https://github.com/estruyf/vscode-front-matter/issues/149): Fix panel rendering when incorrect type for keywords is provided
## [5.1.0] - 2021-10-13
### 🎨 Enhancements
- [#141](https://github.com/estruyf/vscode-front-matter/issues/141): Allow content creation for page bundles or single files
- [#145](https://github.com/estruyf/vscode-front-matter/issues/145): Moved folder registration settings to `frontmatter.json` file
- [#147](https://github.com/estruyf/vscode-front-matter/issues/147): Error boundary added for metadata fields
### 🐞 Fixes
- Rendered more hooks than during the previous render in `FileList`
- [#142](https://github.com/estruyf/vscode-front-matter/issues/142): Fix for unknown tags where it throws an error
- [#143](https://github.com/estruyf/vscode-front-matter/issues/143): Fix for duplicate values in the file list
- [#144](https://github.com/estruyf/vscode-front-matter/issues/144): Fix for `toISOString` does not exist on object
- [#146](https://github.com/estruyf/vscode-front-matter/issues/146): Date parsing logic added with fallbacks
## [5.0.0] - 2021-10-07 - [Release Notes](https://beta.frontmatter.codes/updates/v5.0.0)
### ✨ New features

View File

@@ -48,6 +48,10 @@ Our main extension features are:
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 5**
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
**Version 4**
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
@@ -102,12 +106,15 @@ If you have the courage to test out the beta features, we made available a beta
</a>
</p>
## 🖤 Sponsors 👇 🤘
## 🖤 Backers & Sponsors 👇 🤘
<p align="center">
<a href="https://github.com/timschps" title="Tim Schaeps">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
</a>
</a>
<a href="https://github.com/zivbk1" title="Bryan Klein">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/6154767" />
</a>
<a href="https://github.com/flikteoh" title="FlikTeoh">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
</a>

View File

@@ -46,6 +46,10 @@ Our main extension features are:
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 5**
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
**Version 4**
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
@@ -100,12 +104,15 @@ If you have the courage to test out the beta features, we made available a beta
</a>
</p>
## 🖤 Sponsors 👇 🤘
## 🖤 Backers & Sponsors 👇 🤘
<p align="center">
<a href="https://github.com/timschps" title="Tim Schaeps">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
</a>
</a>
<a href="https://github.com/zivbk1" title="Bryan Klein">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/6154767" />
</a>
<a href="https://github.com/flikteoh" title="FlikTeoh">
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
</a>

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M6 15h15" />
<path d="M21 19h-15" />
<path d="M15 11h6" />
<path d="M21 7h-6" />
<path d="M9 9h1a1 1 0 1 1 -1 1v-2.5a2 2 0 0 1 2 -2" />
<path d="M3 9h1a1 1 0 1 1 -1 1v-2.5a2 2 0 0 1 2 -2" />
</svg>

After

Width:  |  Height:  |  Size: 449 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M6 15h15" />
<path d="M21 19h-15" />
<path d="M15 11h6" />
<path d="M21 7h-6" />
<path d="M9 9h1a1 1 0 1 1 -1 1v-2.5a2 2 0 0 1 2 -2" />
<path d="M3 9h1a1 1 0 1 1 -1 1v-2.5a2 2 0 0 1 2 -2" />
</svg>

After

Width:  |  Height:  |  Size: 449 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M7 5h6a3.5 3.5 0 0 1 0 7h-6z" />
<path d="M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7" />
</svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M7 5h6a3.5 3.5 0 0 1 0 7h-6z" />
<path d="M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7" />
</svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<polyline points="7 8 3 12 7 16" />
<polyline points="17 8 21 12 17 16" />
<line x1="14" y1="4" x2="10" y2="20" />
</svg>

After

Width:  |  Height:  |  Size: 358 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<polyline points="7 8 3 12 7 16" />
<polyline points="17 8 21 12 17 16" />
<line x1="14" y1="4" x2="10" y2="20" />
</svg>

After

Width:  |  Height:  |  Size: 358 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M9 12h6" />
<path d="M12 9v6" />
<path d="M6 19a2 2 0 0 1 -2 -2v-4l-1 -1l1 -1v-4a2 2 0 0 1 2 -2" />
<path d="M18 19a2 2 0 0 0 2 -2v-4l1 -1l-1 -1v-4a2 2 0 0 0 -2 -2" />
</svg>

After

Width:  |  Height:  |  Size: 422 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M9 12h6" />
<path d="M12 9v6" />
<path d="M6 19a2 2 0 0 1 -2 -2v-4l-1 -1l1 -1v-4a2 2 0 0 1 2 -2" />
<path d="M18 19a2 2 0 0 0 2 -2v-4l1 -1l-1 -1v-4a2 2 0 0 0 -2 -2" />
</svg>

After

Width:  |  Height:  |  Size: 422 B

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve">
<rect x="25" y="25" fill="none" stroke="#ffffff" stroke-width="50" stroke-miterlimit="10" width="1200" height="1200"/>
<path fill="#ffffff" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
<path fill="#ffffff" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve">
<rect x="25" y="25" fill="none" stroke="#424242" stroke-width="50" stroke-miterlimit="10" width="1200" height="1200"/>
<path fill="#424242" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
<path fill="#424242" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve" width="16" height="16">
<path fill="#C5C5C5" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
<path fill="#C5C5C5" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve" width="16" height="16">
<path fill="#424242" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
<path fill="#424242" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve" width="16" height="16">
<path fill="#02aeb7" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
<path fill="#02aeb7" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M7 12h10" />
<path d="M7 4v16" />
<path d="M17 4v16" />
<path d="M15 20h4" />
<path d="M15 4h4" />
<path d="M5 20h4" />
<path d="M5 4h4" />
</svg>

After

Width:  |  Height:  |  Size: 400 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M7 12h10" />
<path d="M7 4v16" />
<path d="M17 4v16" />
<path d="M15 20h4" />
<path d="M15 4h4" />
<path d="M5 20h4" />
<path d="M5 4h4" />
</svg>

After

Width:  |  Height:  |  Size: 400 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<line x1="11" y1="5" x2="17" y2="5" />
<line x1="7" y1="19" x2="13" y2="19" />
<line x1="14" y1="5" x2="10" y2="19" />
</svg>

After

Width:  |  Height:  |  Size: 362 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<line x1="11" y1="5" x2="17" y2="5" />
<line x1="7" y1="19" x2="13" y2="19" />
<line x1="14" y1="5" x2="10" y2="19" />
</svg>

After

Width:  |  Height:  |  Size: 362 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="#C5C5C5" width="24" height="24">
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 269 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="#424242" width="24" height="24">
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 269 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="8" y2="12.01" />
<line x1="12" y1="12" x2="12" y2="12.01" />
<line x1="16" y1="12" x2="16" y2="12.01" />
</svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="8" y2="12.01" />
<line x1="12" y1="12" x2="12" y2="12.01" />
<line x1="16" y1="12" x2="16" y2="12.01" />
</svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-list-numbers" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M11 6h9" />
<path d="M11 12h9" />
<path d="M12 18h8" />
<path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4" />
<path d="M6 10v-6l-2 2" />
</svg>

After

Width:  |  Height:  |  Size: 451 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-list-numbers" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M11 6h9" />
<path d="M11 12h9" />
<path d="M12 18h8" />
<path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4" />
<path d="M6 10v-6l-2 2" />
</svg>

After

Width:  |  Height:  |  Size: 451 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M7 5v9a5 5 0 0 0 10 0v-9" />
<path d="M4 12h16" />
</svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M7 5v9a5 5 0 0 0 10 0v-9" />
<path d="M4 12h16" />
</svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-list" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<line x1="9" y1="6" x2="20" y2="6" />
<line x1="9" y1="12" x2="20" y2="12" />
<line x1="9" y1="18" x2="20" y2="18" />
<line x1="5" y1="6" x2="5" y2="6.01" />
<line x1="5" y1="12" x2="5" y2="12.01" />
<line x1="5" y1="18" x2="5" y2="18.01" />
</svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-list" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#424242" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<line x1="9" y1="6" x2="20" y2="6" />
<line x1="9" y1="12" x2="20" y2="12" />
<line x1="9" y1="18" x2="20" y2="18" />
<line x1="5" y1="6" x2="5" y2="6.01" />
<line x1="5" y1="12" x2="5" y2="12.01" />
<line x1="5" y1="18" x2="5" y2="18.01" />
</svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@@ -356,8 +356,18 @@
text-transform: capitalize;
}
.table__cell__seo_details {
padding: 10px;
}
.table__cell__validation {
text-align: center;
text-align: left;
}
.table__cell__validation div {
display: flex;
align-items: center;
padding: 2px 0;
}
.table__cell__validation .valid {
@@ -368,6 +378,15 @@
color: #E6AF2E;
}
.table__cell__validation div span + span {
margin-left: .5rem;
}
.seo__status__note {
font-size: 10px;
padding: 3px 0;
}
/* Fields */
.field__toggle {
position: relative;
@@ -437,6 +456,25 @@ input:checked + .field__toggle__slider:before {
margin-right: .5rem;
}
.metadata_field__error {
color: var(--vscode-errorForeground);
display: flex;
justify-content: space-between;
align-items: center;
}
.metadata_field__error button {
color: var(--vscode-button-secondaryForeground);
background-color: var(--vscode-button-secondaryBackground);
padding-left: 1rem;
padding-right: 1rem;
width: auto;
}
.metadata_field__error button:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.metadata_field__input, .metadata_field__input:focus,
.metadata_field__textarea, .metadata_field__textarea:focus {
outline: none;

20769
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{
"name": "vscode-front-matter-beta",
"displayName": "Front Matter",
"description": "An essential Visual Studio Code extension when you want to manage the markdown pages of your static site like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "5.0.0",
"version": "5.10.0",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -23,7 +23,7 @@
}
],
"engines": {
"vscode": "^1.51.0"
"vscode": "^1.63.0"
},
"categories": [
"Other"
@@ -66,7 +66,7 @@
"onCommand:frontMatter.insertImage",
"onView:frontMatter.explorer"
],
"main": "./dist/extension",
"main": "./dist/extension.js",
"contributes": {
"viewsContainers": {
"activitybar": [
@@ -97,10 +97,76 @@
"markdownDescription": "Specify if you want to automatically update the modified date of your article/page. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.autoupdatedate)",
"scope": "Content"
},
"frontMatter.content.defaultFileType": {
"type": "string",
"default": "md",
"enum": [
"md",
"mdx"
],
"markdownDescription": "Specify the default file type for the content to create. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.defaultfiletype)",
"scope": "Content"
},
"frontMatter.content.defaultSorting": {
"type": "string",
"default": "",
"oneOf": [
{
"enum": [
"LastModifiedAsc",
"LastModifiedDesc",
"FileNameAsc",
"FileNameDesc"
]
},
{
"type": "string"
}
],
"markdownDescription": "Specify the default sorting option for the content dashboard. You can use one of the values from the enum or define your own ID. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.defaultSorting)",
"scope": "Content"
},
"frontMatter.content.draftField": {
"type": "object",
"markdownDescription": "Define the draft field you want to use to manage your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.draftfield)",
"default": {
"name": "draft",
"type": "boolean"
},
"properties": {
"type": {
"type": "string",
"enum": [
"boolean",
"choice"
],
"description": "Type of the draft field you want to use"
},
"name": {
"type": "string",
"description": "Name of the field to use"
},
"choices": {
"type": "array",
"description": "List of choices for the field",
"items": {
"type": [
"string"
]
}
}
},
"additionalProperties": false,
"required": [
"type",
"name"
],
"scope": "Content"
},
"frontMatter.content.fmHighlight": {
"type": "boolean",
"default": true,
"markdownDescription": "Specify if you want to highlight the Front Matter in the Markdown file. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.fmhighlight)",
"markdownDescription": "Specify if you want to highlight the Front Matter in the Markdown file. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.fmhighlight)",
"scope": "Content"
},
"frontMatter.content.pageFolders": {
@@ -117,6 +183,11 @@
"path": {
"type": "string",
"description": "Path of the folder"
},
"excludeSubdir": {
"type": "boolean",
"default": false,
"description": "Exclude sub-directories"
}
},
"additionalProperties": false,
@@ -133,6 +204,58 @@
"markdownDescription": "Specify the folder name where all your assets are located. For instance in Hugo this is the `static` folder. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.publicfolder)",
"scope": "Content"
},
"frontMatter.content.sorting": {
"type": "array",
"default": [],
"markdownDescription": "Define the sorting options for your dashboard content. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.sorting)",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The ID of the sorting option. This will be used for the storing the last used sorting option or the default option."
},
"title": {
"type": "string",
"description": "Name of the sorting label"
},
"name": {
"type": "string",
"description": "Name of the metadata field to sort by"
},
"order": {
"type": "string",
"enum": [
"asc",
"desc"
],
"description": "Order of the sorting"
},
"type": {
"type": "string",
"default": "string",
"enum": [
"string",
"date"
],
"description": "Type of the field value"
}
},
"additionalProperties": false,
"required": [
"title",
"name",
"order"
]
},
"scope": "Content"
},
"frontMatter.content.wysiwyg": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies if you want to enable/disable the What You See, Is What You Get (WYSIWYG) markdown controls. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.wysiwyg)",
"scope": "Content"
},
"frontMatter.custom.scripts": {
"type": "array",
"default": [],
@@ -151,6 +274,32 @@
"nodeBin": {
"type": "string",
"description": "Path to the node executable. This is required when using NVM, so that there is no confusion of which node version to use."
},
"bulk": {
"type": "boolean",
"description": "Run the script for all content files"
},
"output": {
"type": "string",
"enum": [
"editor",
"notification"
],
"description": "Define where you want to output your script output. Default is a notification, but you can specify to show it in an editor panel."
},
"outputType": {
"type": "string",
"description": "The type of output for the editor panel. Can be used to change it to 'markdown' for example"
},
"type": {
"type": "string",
"default": "content",
"enum": [
"content",
"mediaFolder",
"mediaFile"
],
"description": "The type for which the script will be used."
}
},
"additionalProperties": false,
@@ -164,7 +313,7 @@
"frontMatter.dashboard.mediaSnippet": {
"type": "array",
"default": [],
"markdownDescription": "Specify the a snippet for your custom media insert markup. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.dashboard.mediaSnippet)",
"markdownDescription": "Specify the a snippet for your custom media insert markup. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.mediasnippet)",
"items": {
"type": "string",
"description": "The parts of your snippet. Use `{mediaUrl}` as placeholder where the path of the image needs to be inserted."
@@ -180,6 +329,23 @@
"markdownDescription": "Specify if you want to open the dashboard when you start VS Code. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.openonstart)",
"scope": "Dashboard"
},
"frontMatter.framework.id": {
"type": "string",
"default": "",
"markdownDescription": "Specify the ID of your static site generator or framework you are using for your website. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.framework.id)"
},
"frontMatter.media.defaultSorting": {
"type": "string",
"default": "",
"enum": [
"LastModifiedAsc",
"LastModifiedDesc",
"FileNameAsc",
"FileNameDesc"
],
"markdownDescription": "Specify the default sorting option for the media dashboard. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.media.defaultsorting)",
"scope": "Content"
},
"frontMatter.panel.freeform": {
"type": "boolean",
"default": true,
@@ -198,6 +364,12 @@
"markdownDescription": "Specify the path you want to add after the host and before your slug. This can be used for instance to include the year/month like: `yyyy/MM`. The date will be generated based on the article its date field value. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.preview.pathname)",
"scope": "Site preview"
},
"frontMatter.site.baseURL": {
"type": "string",
"default": "",
"markdownDescription": "Specify the base URL of your site, this will be used for SEO checks. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.site.baseurl)",
"scope": "Site"
},
"frontMatter.taxonomy.alignFilename": {
"type": "boolean",
"default": false,
@@ -214,7 +386,7 @@
},
"frontMatter.taxonomy.commaSeparatedFields": {
"type": "array",
"markdownDescription": "Specify the fields names that Front Matter should treat as a comma-separated array. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.taxonomy.commaSeparatedFields)",
"markdownDescription": "Specify the fields names that Front Matter should treat as a comma-separated array. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.commaSeparatedFields)",
"items": {
"type": "string",
"description": "Name of the fields you want to use as comma-separated arrays."
@@ -226,7 +398,7 @@
"array",
"null"
],
"markdownDescription": "Specify the type of contents you want to use for your articles/pages/etc. Make sure the `type` is correctly set in your front matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.taxonomy.contentTypes)",
"markdownDescription": "Specify the type of contents you want to use for your articles/pages/etc. Make sure the `type` is correctly set in your front matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.contentTypes)",
"items": {
"type": "object",
"description": "Define the content types you want to use in Front Matter.",
@@ -235,6 +407,15 @@
"type": "string",
"description": "Define the type of field"
},
"fileType": {
"type": "string",
"default": "",
"enum": [
"md",
"mdx"
],
"description": "Specifies the type of content you want to create."
},
"fields": {
"type": "array",
"description": "Define the fields of the content type",
@@ -251,8 +432,10 @@
"boolean",
"image",
"choice",
"taxonomy",
"tags",
"categories"
"categories",
"draft"
],
"description": "Define the type of field"
},
@@ -306,14 +489,62 @@
"type": "boolean",
"default": false,
"description": "Do you want to hide the field from the metadata section?"
},
"taxonomyId": {
"type": "string",
"default": "",
"description": "The ID of your taxonomy field"
}
},
"additionalProperties": false,
"required": [
"type",
"name"
],
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "taxonomy"
}
}
},
"then": {
"required": [
"taxonomyId"
]
}
},
{
"if": {
"properties": {
"type": {
"const": "choice"
}
}
},
"then": {
"required": [
"choices"
]
}
}
]
}
},
"pageBundle": {
"type": "boolean",
"default": false,
"description": "Specify if you want to create a folder when creating new content."
},
"previewPath": {
"type": [
"null",
"string"
],
"default": null,
"description": "Defines a custom preview path for the content type."
}
},
"additionalProperties": false,
@@ -325,6 +556,7 @@
"default": [
{
"name": "default",
"pageBundle": false,
"fields": [
{
"title": "Title",
@@ -366,6 +598,32 @@
],
"scope": "Taxonomy"
},
"frontMatter.taxonomy.customTaxonomy": {
"type": "array",
"markdownDescription": "Specify the custom taxonomy field data. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.tags)",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID for your taxonomy field"
},
"options": {
"type": "array",
"description": "Options from which you can pick",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"required": [
"id",
"options"
]
},
"scope": "Taxonomy"
},
"frontMatter.taxonomy.dateField": {
"type": "string",
"default": "date",
@@ -429,6 +687,12 @@
"markdownDescription": "Specifies the optimal description length for SEO (set to `-1` to turn it off). [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.seodescriptionlength)",
"scope": "Taxonomy"
},
"frontMatter.taxonomy.seoSlugLength": {
"type": "number",
"default": 75,
"markdownDescription": "Specifies the optimal slug length for SEO (set to `-1` to turn it off). [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.seoSlugLength)",
"scope": "Taxonomy"
},
"frontMatter.taxonomy.seoTitleLength": {
"type": "number",
"default": 60,
@@ -464,6 +728,24 @@
"default": "yyyy-MM-dd",
"markdownDescription": "Specify the prefix you want to add for your new article filenames. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.prefix)",
"scope": "Templates"
},
"frontMatter.global.notifications": {
"type": "array",
"items": {
"type": "string",
"enum": [
"info",
"warning",
"error"
]
},
"default": [
"info",
"warning",
"error"
],
"markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notifications)",
"scope": "Templates"
}
}
},
@@ -528,7 +810,10 @@
"command": "frontMatter.insertImage",
"title": "Insert image into your content",
"category": "Front matter",
"icon": "$(device-camera)"
"icon": {
"dark": "/assets/icons/media-dark.svg",
"light": "/assets/icons/media-light.svg"
}
},
{
"command": "frontMatter.insertTags",
@@ -544,7 +829,28 @@
"command": "frontMatter.dashboard",
"title": "Open dashboard",
"category": "Front matter",
"icon": "$(preview)"
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.dashboard.media",
"title": "Open media dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.dashboard.close",
"title": "Close dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-teal.svg",
"light": "/assets/icons/frontmatter-small-teal.svg"
}
},
{
"command": "frontMatter.preview",
@@ -565,19 +871,179 @@
"command": "frontMatter.setLastModifiedDate",
"title": "Set lastmod date",
"category": "Front matter"
},
{
"command": "frontMatter.markup.bold",
"title": "Bold",
"category": "Front matter",
"icon": {
"light": "assets/icons/bold-light.svg",
"dark": "assets/icons/bold-dark.svg"
}
},
{
"command": "frontMatter.markup.italic",
"title": "Italic",
"category": "Front matter",
"icon": {
"light": "assets/icons/italic-light.svg",
"dark": "assets/icons/italic-dark.svg"
}
},
{
"command": "frontMatter.markup.strikethrough",
"title": "Strikethrough",
"category": "Front matter",
"icon": {
"light": "assets/icons/strikethrough-light.svg",
"dark": "assets/icons/strikethrough-dark.svg"
}
},
{
"command": "frontMatter.markup.code",
"title": "Code",
"category": "Front matter",
"icon": {
"light": "assets/icons/code-light.svg",
"dark": "assets/icons/code-dark.svg"
}
},
{
"command": "frontMatter.markup.codeblock",
"title": "Codeblock",
"category": "Front matter",
"icon": {
"light": "assets/icons/codeblock-light.svg",
"dark": "assets/icons/codeblock-dark.svg"
}
},
{
"command": "frontMatter.markup.blockquote",
"title": "Codeblock",
"category": "Front matter",
"icon": {
"light": "assets/icons/blockquote-light.svg",
"dark": "assets/icons/blockquote-dark.svg"
}
},
{
"command": "frontMatter.markup.heading",
"title": "Heading",
"category": "Front matter",
"icon": {
"light": "assets/icons/heading-light.svg",
"dark": "assets/icons/heading-dark.svg"
}
},
{
"command": "frontMatter.markup.unorderedlist",
"title": "Unordered list",
"category": "Front matter",
"icon": {
"light": "assets/icons/unordered-list-light.svg",
"dark": "assets/icons/unordered-list-dark.svg"
}
},
{
"command": "frontMatter.markup.orderedlist",
"title": "Ordered list",
"category": "Front matter",
"icon": {
"light": "assets/icons/ordered-list-light.svg",
"dark": "assets/icons/ordered-list-dark.svg"
}
},
{
"command": "frontMatter.markup.tasklist",
"title": "Task list",
"category": "Front matter"
},
{
"command": "frontMatter.markup.options",
"title": "Other markup options",
"category": "Front matter",
"icon": {
"light": "assets/icons/options-light.svg",
"dark": "assets/icons/options-dark.svg"
}
},
{
"command": "frontMatter.diagnostics",
"title": "Diagnostic logging",
"category": "Front matter"
}
],
"menus": {
"editor/title": [
{
"command": "frontMatter.markup.heading",
"group": "navigation@-132",
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.markup.bold",
"group": "navigation@-131",
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.markup.italic",
"group": "navigation@-130",
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.markup.strikethrough",
"group": "navigation@-129",
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.markup.blockquote",
"group": "navigation@-128",
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.insertImage",
"group": "navigation@-99",
"group": "navigation@-127",
"when": "resourceLangId == markdown"
},
{
"command": "frontMatter.markup.options",
"group": "navigation@-126",
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.markup.orderedlist",
"group": "1_markup@1",
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.markup.unorderedlist",
"group": "1_markup@2",
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.markup.tasklist",
"group": "1_markup@3",
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.markup.code",
"group": "1_markup@4",
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.markup.codeblock",
"group": "1_markup@5",
"when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg"
},
{
"command": "frontMatter.dashboard",
"group": "navigation@-98",
"when": "frontMatter:enabled == true"
"when": "frontMatter:enabled == true && frontMatter:dashboard:open == false"
},
{
"command": "frontMatter.dashboard.close",
"group": "navigation@-98",
"when": "frontMatter:enabled == true && frontMatter:dashboard:open == true"
}
],
"explorer/context": [
@@ -625,12 +1091,60 @@
{
"command": "frontMatter.createFromTemplate",
"when": "false"
},
{
"command": "frontMatter.dashboard.close",
"when": "false"
},
{
"command": "frontMatter.markup.bold",
"when": "false"
},
{
"command": "frontMatter.markup.italic",
"when": "false"
},
{
"command": "frontMatter.markup.strikethrough",
"when": "false"
},
{
"command": "frontMatter.markup.code",
"when": "false"
},
{
"command": "frontMatter.markup.codeblock",
"when": "false"
},
{
"command": "frontMatter.markup.blockquote",
"when": "false"
},
{
"command": "frontMatter.markup.heading",
"when": "false"
},
{
"command": "frontMatter.markup.unorderedlist",
"when": "false"
},
{
"command": "frontMatter.markup.orderedlist",
"when": "false"
},
{
"command": "frontMatter.markup.tasklist",
"when": "false"
},
{
"command": "frontMatter.markup.options",
"when": "false"
}
],
"view/title": [
{
"command": "frontMatter.collapseSections",
"group": "navigation"
"group": "frontmatter-explorer"
}
]
},
@@ -645,9 +1159,15 @@
]
},
"scripts": {
"vscode:prepublish": "npm run clean && webpack --mode production",
"build:ext": "npm run clean && webpack --mode development",
"dev:ext": "npm run clean && webpack --mode development --watch",
"dev:ext": "npm run clean && npm-run-all --parallel watch:*",
"vscode:prepublish": "npm run clean && npm-run-all --parallel prod:*",
"build:ext": "npm run clean && npm-run-all --parallel dev:build:*",
"watch:ext": "webpack --mode development --watch --config ./webpack/extension.config.js",
"watch:dashboard": "webpack serve --mode development --config ./webpack/dashboard.config.js",
"dev:build:ext": "webpack --mode development --config ./webpack/extension.config.js",
"dev:build:dashboard": "webpack --mode development --config ./webpack/dashboard.config.js",
"prod:ext": "webpack --mode production --config ./webpack/extension.config.js",
"prod:dashboard": "webpack --mode production --config ./webpack/dashboard.config.js",
"test-compile": "tsc -p ./",
"clean": "rimraf dist",
"start:site": "cd ./docs && npm run dev"
@@ -658,8 +1178,8 @@
"@headlessui/react": "^1.4.1",
"@heroicons/react": "1.0.4",
"@iarna/toml": "2.2.3",
"@sentry/react": "^6.13.2",
"@sentry/tracing": "^6.13.2",
"@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3",
"@tailwindcss/forms": "^0.3.3",
"@types/glob": "7.1.3",
"@types/js-yaml": "3.12.1",
@@ -669,8 +1189,10 @@
"@types/react": "17.0.0",
"@types/react-datepicker": "^4.1.7",
"@types/react-dom": "17.0.0",
"@types/vscode": "1.51.0",
"@types/vscode": "^1.63.0",
"@vscode/codicons": "0.0.20",
"@vscode/webview-ui-toolkit": "^0.8.1",
"@webpack-cli/serve": "^1.6.0",
"autoprefixer": "^10.3.2",
"css-loader": "5.2.7",
"date-fns": "2.23.0",
@@ -681,9 +1203,12 @@
"html-loader": "1.3.2",
"html-webpack-plugin": "4.5.0",
"image-size": "^1.0.0",
"lodash-es": "^4.17.21",
"lodash.uniqby": "4.7.0",
"mdast-util-from-markdown": "1.0.0",
"node-json-db": "^1.3.0",
"npm-run-all": "^4.1.5",
"path-browserify": "^1.0.1",
"postcss": "^8.3.6",
"postcss-loader": "4.3.0",
"react": "17.0.1",
@@ -697,11 +1222,11 @@
"ts-loader": "8.0.3",
"tslint": "6.1.3",
"typescript": "4.0.2",
"url-join-ts": "^1.0.5",
"wc-react": "github:estruyf/wc-react",
"webpack": "4.44.2",
"webpack-cli": "3.3.12"
},
"dependencies": {
"@docsearch/js": "^3.0.0-alpha.40"
"webpack": "^5.65.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.6.0"
}
}

View File

@@ -1,3 +1,4 @@
import { isValidFile } from './../helpers/isValidFile';
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX, CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX } from './../constants';
import * as vscode from 'vscode';
import { TaxonomyType } from "../models";
@@ -5,16 +6,15 @@ import { format } from "date-fns";
import { ArticleHelper, Settings, SlugHelper } from '../helpers';
import matter = require('gray-matter');
import { Notifications } from '../helpers/Notifications';
import { extname, basename } from 'path';
import { extname, basename, parse, dirname } from 'path';
import { COMMAND_NAME, DefaultFields } from '../constants';
import { DashboardData } from '../models/DashboardData';
import { ExplorerView } from '../explorerView/ExplorerView';
import { DateHelper } from '../helpers/DateHelper';
import { parseWinPath } from '../helpers/parseWinPath';
export class Article {
private static prevContent = "";
/**
* Insert taxonomy
*
@@ -116,7 +116,37 @@ export class Article {
return;
}
const article = ArticleHelper.getFrontMatter(editor);
const updatedArticle = this.setLastModifiedDateInner(editor.document);
if (typeof updatedArticle === "undefined") {
return;
}
ArticleHelper.update(
editor,
updatedArticle as matter.GrayMatterFile<string>
);
}
public static async setLastModifiedDateOnSave(
document: vscode.TextDocument
): Promise<vscode.TextEdit[]> {
const updatedArticle = this.setLastModifiedDateInner(document);
if (typeof updatedArticle === "undefined") {
return [];
}
const update = ArticleHelper.generateUpdate(document, updatedArticle);
return [update];
}
private static setLastModifiedDateInner(
document: vscode.TextDocument
): matter.GrayMatterFile<string> | undefined {
const article = ArticleHelper.getFrontMatterFromDocument(document);
if (!article) {
return;
}
@@ -125,8 +155,7 @@ export class Article {
const dateField = Settings.get(SETTING_MODIFIED_FIELD) as string || DefaultFields.LastModified;
try {
cloneArticle.data[dateField] = Article.formatDate(new Date());
ArticleHelper.update(editor, cloneArticle);
return cloneArticle;
} catch (e: any) {
Notifications.error(`Something failed while parsing the date format. Check your "${CONFIG_KEY}${SETTING_DATE_FORMAT}" setting.`);
}
@@ -171,7 +200,7 @@ export class Article {
let newFileName = `${slugName}${ext}`;
if (filePrefix && typeof filePrefix === "string") {
newFileName = `${format(new Date(), filePrefix)}-${newFileName}`;
newFileName = `${format(new Date(), DateHelper.formatUpdate(filePrefix) as string)}-${newFileName}`;
}
const newPath = editor.document.uri.fsPath.replace(fileName, newFileName);
@@ -182,7 +211,7 @@ export class Article {
await vscode.workspace.fs.rename(editor.document.uri, vscode.Uri.file(newPath), {
overwrite: false
});
} catch (e) {
} catch (e: any) {
Notifications.error(`Failed to rename file: ${e?.message || e}`);
}
}
@@ -190,6 +219,31 @@ export class Article {
}
}
/**
* Retrieve the slug from the front matter
*/
public static getSlug() {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const file = parseWinPath(editor.document.fileName);
if (!isValidFile(file)) {
return;
}
const parsedFile = parse(file);
if (parsedFile.name.toLowerCase() !== "index") {
return parsedFile.name;
}
const folderName = basename(dirname(file));
return folderName;
}
/**
* Toggle the page its draft mode
*/
@@ -210,42 +264,29 @@ export class Article {
/**
* Article auto updater
* @param fileChanges
* @param event
*/
public static async autoUpdate(fileChanges: vscode.TextDocumentChangeEvent) {
const txtChanges = fileChanges.contentChanges.map(c => c.text);
const editor = vscode.window.activeTextEditor;
public static async autoUpdate(event: vscode.TextDocumentWillSaveEvent) {
const document = event.document;
if (document && ArticleHelper.isMarkdownFile(document)) {
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
if (txtChanges.length > 0 && editor && ArticleHelper.isMarkdownFile()) {
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
if (autoUpdate) {
const article = ArticleHelper.getFrontMatter(editor);
if (!article) {
return;
}
if (article.content === Article.prevContent) {
return;
}
Article.prevContent = article.content;
Article.setLastModifiedDate();
if (autoUpdate) {
event.waitUntil(Article.setLastModifiedDateOnSave(document));
}
}
}
}
/**
* Format the date to the defined format
*/
public static formatDate(dateValue: Date) {
public static formatDate(dateValue: Date): string {
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
if (dateFormat && typeof dateFormat === "string") {
return format(dateValue, dateFormat);
return format(dateValue, DateHelper.formatUpdate(dateFormat) as string);
} else {
return dateValue.toISOString();
return typeof dateValue.toISOString === 'function' ? dateValue.toISOString() : dateValue?.toString();
}
}

View File

@@ -1,43 +1,27 @@
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME } from '../constants';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { basename, dirname, extname, join } from "path";
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env, Position } from "vscode";
import { Settings as SettingsHelper } from '../helpers';
import { TaxonomyType } from '../models';
import { Folders } from './Folders';
import { PagesListener } from './../listeners/PagesListener';
import { ExtensionListener } from './../listeners/ExtensionListener';
import { SETTINGS_DASHBOARD_OPENONSTART, CONTEXT } from '../constants';
import { join } from "path";
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window } from "vscode";
import { Logger, Settings as SettingsHelper } from '../helpers';
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
import { Page } from '../dashboardWebView/models/Page';
import { openFileInEditor } from '../helpers/openFileInEditor';
import { Template } from './Template';
import { Notifications } from '../helpers/Notifications';
import { Settings } from '../dashboardWebView/models/Settings';
import { Extension } from '../helpers/Extension';
import { parseJSON } from 'date-fns';
import { ViewType } from '../dashboardWebView/state';
import { EditorHelper, WebviewHelper } from '@estruyf/vscode';
import { MediaInfo, MediaPaths } from './../models/MediaPaths';
import { decodeBase64Image } from '../helpers/decodeBase64Image';
import { WebviewHelper } from '@estruyf/vscode';
import { DashboardData } from '../models/DashboardData';
import { ExplorerView } from '../explorerView/ExplorerView';
import { MediaLibrary } from '../helpers/MediaLibrary';
import imageSize from 'image-size';
import { parseWinPath } from '../helpers/parseWinPath';
import { DashboardListener, MediaListener, SettingsListener } from '../listeners';
export class Dashboard {
private static webview: WebviewPanel | null = null;
private static isDisposed: boolean = true;
private static media: MediaInfo[] = [];
private static timers: { [folder: string]: any } = {};
private static _viewData: DashboardData | undefined;
private static mediaLib: MediaLibrary;
private static isDisposed: boolean = true;
public static get viewData(): DashboardData | undefined {
return Dashboard._viewData;
}
/** 
/**
* Init the dashboard
*/
public static async init() {
@@ -51,15 +35,17 @@ export class Dashboard {
* Open or reveal the dashboard
*/
public static async open(data?: DashboardData) {
this.mediaLib = MediaLibrary.getInstance();
MediaLibrary.getInstance();
Dashboard._viewData = data;
if (Dashboard.isOpen) {
Dashboard.reveal();
Dashboard.reveal(!!data);
} else {
Dashboard.create();
}
await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, true);
}
/**
@@ -72,11 +58,33 @@ export class Dashboard {
/**
* Reveal the dashboard if it is open
*/
public static reveal() {
public static reveal(hasData: boolean = false) {
if (Dashboard.webview) {
Dashboard.webview.reveal();
if (hasData) {
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: Dashboard.viewData });
}
}
}
public static close() {
Dashboard.webview?.dispose();
}
public static reload() {
if (Dashboard.isOpen) {
Dashboard.webview?.dispose();
setTimeout(() => {
Dashboard.open();
}, 100);
}
}
public static resetViewData() {
Dashboard._viewData = undefined;
}
/**
* Create the dashboard webview
@@ -90,494 +98,68 @@ export class Dashboard {
'FrontMatter Dashboard',
ViewColumn.One,
{
enableScripts: true
enableScripts: true,
retainContextWhenHidden: true
}
);
Dashboard.isDisposed = false;
Dashboard.webview.iconPath = {
dark: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter-dark.svg')),
light: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter.svg'))
dark: Uri.file(join(extensionUri.fsPath, 'assets/icons/frontmatter-short-dark.svg')),
light: Uri.file(join(extensionUri.fsPath, 'assets/icons/frontmatter-short-light.svg'))
};
Dashboard.webview.webview.html = Dashboard.getWebviewContent(Dashboard.webview.webview, extensionUri);
Dashboard.webview.onDidChangeViewState(() => {
Dashboard.webview.onDidChangeViewState(async () => {
if (!this.webview?.visible) {
Dashboard._viewData = undefined;
const panel = ExplorerView.getInstance(extensionUri);
panel.getMediaSelection();
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: null });
}
await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, this.webview?.visible);
});
Dashboard.webview.onDidDispose(() => {
Dashboard.webview.onDidDispose(async () => {
Dashboard.isDisposed = true;
Dashboard._viewData = undefined;
const panel = ExplorerView.getInstance(extensionUri);
panel.getMediaSelection();
await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, false);
});
SettingsHelper.onConfigChange((global?: any) => {
Dashboard.getSettings();
SettingsListener.getSettings();
});
Dashboard.webview.webview.onDidReceiveMessage(async (msg) => {
switch(msg.command) {
case DashboardMessage.getViewType:
if (Dashboard._viewData) {
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: Dashboard._viewData });
}
break;
case DashboardMessage.getData:
Dashboard.getSettings();
Dashboard.getPages();
break;
case DashboardMessage.openFile:
openFileInEditor(msg.data);
break;
case DashboardMessage.createContent:
await commands.executeCommand(COMMAND_NAME.createContent);
break;
case DashboardMessage.createByContentType:
await commands.executeCommand(COMMAND_NAME.createByContentType);
break;
case DashboardMessage.createByTemplate:
await commands.executeCommand(COMMAND_NAME.createByTemplate);
break;
case DashboardMessage.updateSetting:
Dashboard.updateSetting(msg.data);
break;
case DashboardMessage.initializeProject:
await commands.executeCommand(COMMAND_NAME.init, Dashboard.getSettings);
break;
case DashboardMessage.reload:
if (!Dashboard.isDisposed) {
Dashboard.webview?.dispose();
setTimeout(() => {
Dashboard.open();
}, 100);
}
break;
case DashboardMessage.setPageViewType:
Extension.getInstance().setState(ExtensionState.PagesView, msg.data);
break;
case DashboardMessage.getMedia:
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder);
break;
case DashboardMessage.copyToClipboard:
env.clipboard.writeText(msg.data);
break;
case DashboardMessage.refreshMedia:
Dashboard.resetMedia();
Dashboard.getMedia(0, msg?.data?.folder);
break;
case DashboardMessage.uploadMedia:
Dashboard.saveFile(msg?.data);
break;
case DashboardMessage.deleteMedia:
Dashboard.deleteFile(msg?.data);
break;
case DashboardMessage.insertPreviewImage:
Dashboard.insertImage(msg?.data);
break;
case DashboardMessage.updateMediaMetadata:
Dashboard.updateMediaMetadata(msg?.data);
break;
case DashboardMessage.createMediaFolder:
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.data);
break;
}
});
}
/**
* Reset media array
*/
public static resetMedia() {
Dashboard.media = [];
}
public static switchFolder(folderPath: string) {
Dashboard.resetMedia();
Dashboard.getMedia(0, folderPath);
}
/**
* Insert an image into the front matter or contents
* @param data
*/
private static async insertImage(data: any) {
if (data?.file && data?.image) {
if (!data?.position) {
await commands.executeCommand(`workbench.view.extension.frontmatter-explorer`);
}
await EditorHelper.showFile(data.file);
Dashboard._viewData = undefined;
Logger.info(`Receiving message from webview: ${msg.command}`);
const extensionUri = Extension.getInstance().extensionPath;
const panel = ExplorerView.getInstance(extensionUri);
if (data?.position) {
const editor = window.activeTextEditor;
const line = data.position.line;
const character = data.position.character;
if (line) {
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || `![${data.alt || data.caption || ""}](${data.image})`));
}
panel.getMediaSelection();
} else {
panel.getMediaSelection();
panel.updateMetadata({field: data.fieldName, value: data.image });
}
}
}
/**
* Retrieve the settings for the dashboard
*/
private static async getSettings() {
const ext = Extension.getInstance();
const wsFolder = Folders.getWorkspaceFolder();
Dashboard.postWebviewMessage({
command: DashboardCommand.settings,
data: {
beta: ext.isBetaVersion(),
wsFolder: wsFolder ? wsFolder.fsPath : '',
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER),
folders: Folders.get(),
initialized: await Template.isInitialized(),
tags: SettingsHelper.getTaxonomy(TaxonomyType.Tag),
categories: SettingsHelper.getTaxonomy(TaxonomyType.Category),
openOnStart: SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART),
versionInfo: ext.getVersion(),
pageViewType: await ext.getState<ViewType | undefined>(ExtensionState.PagesView),
mediaSnippet: SettingsHelper.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
contentFolders: Folders.get().map(f => f.path),
} as Settings
DashboardListener.process(msg);
ExtensionListener.process(msg);
MediaListener.process(msg);
PagesListener.process(msg);
SettingsListener.process(msg);
});
}
/**
* Update a setting from the dashboard
* Return the webview
* @returns The webview
*/
private static async updateSetting(data: { name: string, value: any }) {
await SettingsHelper.update(data.name, data.value);
Dashboard.getSettings();
}
/**
* Retrieve all media files
*/
private static async getMedia(page: number = 0, selectedFolder: string = '') {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const contentFolders = Folders.get();
// If the static folder is not set, retreive the last opened location
if (!selectedFolder) {
const stateValue = await Extension.getInstance().getState<string | undefined>(ExtensionState.SelectedFolder);
if (stateValue && existsSync(stateValue)) {
selectedFolder = stateValue;
}
}
// Go to the home folder
if (selectedFolder === HOME_PAGE_NAVIGATION_ID) {
selectedFolder = '';
}
const relSelectedFolderPath = selectedFolder ? selectedFolder.substring((parseWinPath(wsFolder?.fsPath || "")).length + 1) : '';
let allMedia: MediaInfo[] = [];
if (relSelectedFolderPath) {
const files = await workspace.findFiles(join(relSelectedFolderPath, '/*'));
const media = Dashboard.filterMedia(files);
allMedia = [...media];
} else {
if (staticFolder) {
const folderSearch = join(staticFolder || "", '/*');
const files = await workspace.findFiles(folderSearch);
const media = Dashboard.filterMedia(files);
allMedia = [...media];
}
if (contentFolders && wsFolder) {
for (let i = 0; i < contentFolders.length; i++) {
const contentFolder = contentFolders[i];
const relFolderPath = contentFolder.path.substring(wsFolder.fsPath.length + 1);
const folderSearch = relSelectedFolderPath ? join(relSelectedFolderPath, '/*') : join(relFolderPath, '/*');
const files = await workspace.findFiles(folderSearch);
const media = Dashboard.filterMedia(files);
allMedia = [...allMedia, ...media];
}
}
}
allMedia = allMedia.sort((a, b) => {
if (b.fsPath < a.fsPath) {
return -1;
}
if (b.fsPath > a.fsPath) {
return 1;
}
return 0;
});
Dashboard.media = Object.assign([], allMedia);
let files: MediaInfo[] = Dashboard.media;
// Retrieve the total after filtering and before the slicing happens
const total = files.length;
// Get media set
files = files.slice(page * 16, ((page + 1) * 16));
files = files.map((file) => {
try {
const metadata = Dashboard.mediaLib.get(file.fsPath);
return {
...file,
stats: statSync(file.fsPath),
dimensions: imageSize(file.fsPath),
...metadata
};
} catch (e) {
return {...file, stats: undefined};
}
});
files = files.filter(f => f.stats !== undefined);
// Retrieve all the folders
let allContentFolders: string[] = [];
let allFolders: string[] = [];
if (selectedFolder) {
if (existsSync(selectedFolder)) {
allFolders = readdirSync(selectedFolder, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(selectedFolder, dir.name)));
}
} else {
for (const contentFolder of contentFolders) {
const contentPath = contentFolder.path;
if (contentPath && existsSync(contentPath)) {
const subFolders = readdirSync(contentPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(contentPath, dir.name)));
allContentFolders = [...allContentFolders, ...subFolders];
}
}
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "");
if (staticPath && existsSync(staticPath)) {
allFolders = readdirSync(staticPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(staticPath, dir.name)));
}
}
// Store the last opened folder
await Extension.getInstance().setState(ExtensionState.SelectedFolder, selectedFolder);
Dashboard.postWebviewMessage({
command: DashboardCommand.media,
data: {
media: files,
total: total,
folders: [...allContentFolders, ...allFolders],
selectedFolder
} as MediaPaths
});
}
/**
* Retrieve all the markdown pages
*/
private static async getPages() {
const wsFolder = Folders.getWorkspaceFolder();
const descriptionField = SettingsHelper.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
const dateField = SettingsHelper.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const folderInfo = await Folders.getInfo();
const pages: Page[] = [];
if (folderInfo) {
for (const folder of folderInfo) {
for (const file of folder.lastModified) {
if (file.fileName.endsWith(`.md`) || file.fileName.endsWith(`.mdx`)) {
try {
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
if (article?.data.title) {
const page: Page = {
...article.data,
// FrontMatter properties
fmFolder: folder.title,
fmModified: file.mtime,
fmFilePath: file.filePath,
fmFileName: file.fileName,
fmDraft: article?.data.draft ? "Draft" : "Published",
fmYear: article?.data[dateField] ? parseJSON(article?.data[dateField]).getFullYear() : null,
// Make sure these are always set
title: article?.data.title,
slug: article?.data.slug,
date: article?.data[dateField] || "",
draft: article?.data.draft,
description: article?.data[descriptionField] || "",
};
const contentType = ArticleHelper.getContentType(article.data);
const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview";
if (article?.data[previewField] && wsFolder) {
let fieldValue = article?.data[previewField];
if (fieldValue && Array.isArray(fieldValue)) {
if (fieldValue.length > 0) {
fieldValue = fieldValue[0];
} else {
fieldValue = undefined;
}
}
// Revalidate as the array could have been empty
if (fieldValue) {
const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue);
const contentFolderPath = join(dirname(file.filePath), fieldValue);
let previewUri = null;
if (existsSync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (existsSync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
}
if (previewUri) {
const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
page[previewField] = preview?.toString() || "";
} else {
page[previewField] = "";
}
}
}
pages.push(page);
}
} catch (error: any) {
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
}
}
}
}
}
Dashboard.postWebviewMessage({
command: DashboardCommand.pages,
data: pages
});
}
/**
* Filter the media files
*/
private static filterMedia(files: Uri[]) {
return files.filter(file => {
const ext = extname(file.fsPath);
return ['.jpg', '.jpeg', '.png', '.gif', '.svg'].includes(ext);
}).map((file) => ({
fsPath: file.fsPath,
vsPath: Dashboard.webview?.webview.asWebviewUri(file).toString(),
stats: undefined
} as MediaInfo));
}
/**
* Save the dropped file in the current folder
* @param fileData
*/
private static async saveFile({fileName, contents, folder}: { fileName: string; contents: string; folder: string | null }) {
if (fileName && contents) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const wsPath = wsFolder ? wsFolder.fsPath : "";
let absFolderPath = join(wsPath, staticFolder || "");
if (folder) {
absFolderPath = folder;
}
if (!existsSync(absFolderPath)) {
absFolderPath = join(wsPath, folder || "");
}
if (!existsSync(absFolderPath)) {
Notifications.error(`We couldn't find your selected folder.`);
return;
}
const staticPath = join(absFolderPath, fileName);
const imgData = decodeBase64Image(contents);
if (imgData) {
writeFileSync(staticPath, imgData.data);
Notifications.info(`File ${fileName} uploaded to: ${staticFolder}/${folder}`);
const folderPath = `${staticFolder}/${folder}`;
if (Dashboard.timers[folderPath]) {
clearTimeout(Dashboard.timers[folderPath]);
delete Dashboard.timers[folderPath];
}
Dashboard.timers[folderPath] = setTimeout(() => {
Dashboard.media = [];
Dashboard.getMedia(0, folder || "");
delete Dashboard.timers[folderPath];
}, 500);
} else {
Notifications.error(`Something went wrong uploading ${fileName}`);
}
}
}
/**
* Delete the selected file
* @param fileData
*/
private static async deleteFile({ file, page, folder }: { file: string; page: number; folder: string | null; }) {
if (!file) {
return;
}
try {
unlinkSync(file);
Dashboard.media = [];
Dashboard.getMedia(page || 0, folder || "");
} catch(err) {
Notifications.error(`Something went wrong deleting ${basename(file)}`);
}
}
/**
* Update the metadata of the selected file
*/
private static async updateMediaMetadata({ file, filename, page, folder, ...metadata }: { file:string; filename:string; page: number; folder: string | null; metadata: any; }) {
Dashboard.mediaLib.set(file, metadata);
// Check if filename needs to be updated
Dashboard.mediaLib.updateFilename(file, filename);
Dashboard.getMedia(page || 0, folder || "");
public static getWebview() {
return Dashboard.webview?.webview;
}
/**
* Post data to the dashboard
* @param msg
*/
private static postWebviewMessage(msg: { command: DashboardCommand, data?: any }) {
public static postWebviewMessage(msg: { command: DashboardCommand, data?: any }) {
if (Dashboard.isDisposed) {
return;
}
@@ -592,27 +174,48 @@ export class Dashboard {
* @param webView
*/
private static getWebviewContent(webView: Webview, extensionPath: Uri): string {
const scriptUri = webView.asWebviewUri(Uri.joinPath(extensionPath, 'dist', 'pages.js'));
const dashboardFile = "dashboardWebView.js";
const localServerUrl = "http://localhost:9000";
let scriptUri = "";
const isProd = Extension.getInstance().isProductionMode;
if (isProd) {
scriptUri = webView.asWebviewUri(Uri.joinPath(extensionPath, 'dist', dashboardFile)).toString();
} else {
scriptUri = `${localServerUrl}/${dashboardFile}`;
}
const nonce = WebviewHelper.getNonce();
const version = Extension.getInstance().getVersion();
const ext = Extension.getInstance();
const version = ext.getVersion();
const isBeta = ext.isBetaVersion();
const csp = [
`default-src 'none';`,
`img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'`,
`script-src ${isProd ? `'nonce-${nonce}'` : "http://localhost:9000 http://0.0.0.0:9000"}`,
`style-src ${webView.cspSource} 'self' 'unsafe-inline'`,
`font-src ${webView.cspSource}`,
`connect-src https://o1022172.ingest.sentry.io ${isProd ? `` : "ws://localhost:9000 ws://0.0.0.0:9000 http://localhost:9000 http://0.0.0.0:9000"}`
];
return `
<!DOCTYPE html>
<html lang="en" style="width:100%;height:100%;margin:0;padding:0;">
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}; connect-src https://o1022172.ingest.sentry.io">
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="${csp.join('; ')}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Front Matter Dashboard</title>
</head>
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden" class="bg-gray-100 text-vulcan-500 dark:bg-vulcan-500 dark:text-whisper-500">
<div id="app" style="width:100%;height:100%;margin:0;padding:0;" ${version.usedVersion ? "" : `data-showWelcome="true"`}></div>
<div id="app" data-isProd="${isProd}" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" style="width:100%;height:100%;margin:0;padding:0;" ${version.usedVersion ? "" : `data-showWelcome="true"`}></div>
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`dashboard-${version.installedVersion}`}" alt="Daily usage" />
<script nonce="${nonce}" src="${scriptUri}"></script>
<script ${isProd ? `nonce="${nonce}"` : ""} src="${scriptUri}"></script>
</body>
</html>
`;

View File

@@ -0,0 +1,63 @@
import { Folders } from "./Folders";
import { ViewColumn, workspace } from "vscode";
import ContentProvider from "../providers/ContentProvider";
import { join } from "path";
import { ContentFolder } from "../models";
export class Diagnostics {
public static async show() {
const folders = Folders.get();
const projectName = Folders.getProjectFolderName();
const wsFolder = Folders.getWorkspaceFolder();
const folderData = [];
for (const folder of folders) {
folderData.push(await Diagnostics.processFolder(folder, projectName));
}
const all = await Diagnostics.allProjectFiles();
const logging = `# Project name
${projectName}
# Folders
${folders.map(f => `- ${f.title}: "${f.path}"`).join("\n")}
# Workspace folder
${wsFolder ? wsFolder.fsPath : "No workspace folder"}
# Total files
${all}
# Folders to search files
${folderData.join("\n")}
`;
ContentProvider.show(logging, `${projectName} diagnostics`, "markdown", ViewColumn.One);
}
private static async allProjectFiles() {
const allFiles = await workspace.findFiles(`**/*.*`);
return `Total files found: ${allFiles.length}`;
}
private static async processFolder(folder: ContentFolder, projectName: string) {
let projectStart = folder.path.split(projectName).pop();
projectStart = projectStart || "";
projectStart = projectStart?.replace(/\\/g, '/');
projectStart = projectStart?.startsWith('/') ? projectStart.substr(1) : projectStart;
const mdFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.md'));
const mdxFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.mdx'));
const markdownFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.markdown'));
return `- Project start length: ${projectStart.length} | Search in: "${join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.*')}" | mdFiles: ${mdFiles.length} | mdxFiles: ${mdxFiles.length} | markdownFiles: ${markdownFiles.length}`;
}
}

View File

@@ -11,6 +11,8 @@ import { existsSync, mkdirSync } from 'fs';
import { format } from 'date-fns';
import { Dashboard } from './Dashboard';
import { parseWinPath } from '../helpers/parseWinPath';
import { MediaHelpers } from '../helpers/MediaHelpers';
import { MediaListener, PagesListener } from '../listeners';
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
@@ -62,7 +64,8 @@ export class Folders {
}
if (Dashboard.isOpen) {
Dashboard.switchFolder(folderName);
MediaHelpers.resetMedia();
MediaListener.sendMediaFiles(0, folderName);
}
}
@@ -207,19 +210,23 @@ export class Folders {
try {
const projectName = Folders.getProjectFolderName();
let projectStart = folder.path.split(projectName).pop();
if (projectStart) {
projectStart = projectStart.replace(/\\/g, '/');
projectStart = projectStart.startsWith('/') ? projectStart.substr(1) : projectStart;
const mdFiles = await workspace.findFiles(join(projectStart, '**/*.md'));
const mdxFiles = await workspace.findFiles(join(projectStart, '**/*.mdx'));
let files = [...mdFiles, ...mdxFiles];
const mdFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.md'));
const markdownFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.markdown'));
const mdxFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.mdx'));
let files = [...mdFiles, ...markdownFiles, ...mdxFiles];
if (files) {
let fileStats: FileInfo[] = [];
for (const file of files) {
try {
const fileName = basename(file.fsPath);
const stats = await workspace.fs.stat(file);
fileStats.push({
filePath: file.fsPath,
fileName,
@@ -263,7 +270,7 @@ export class Folders {
const folders: ContentFolder[] = Settings.get(SETTINGS_CONTENT_PAGE_FOLDERS) as ContentFolder[];
return folders.map(folder => ({
title: folder.title,
...folder,
path: Folders.absWsFolder(folder, wsFolder)
}));
}
@@ -274,7 +281,16 @@ export class Folders {
*/
private static async update(folders: ContentFolder[]) {
const wsFolder = Folders.getWorkspaceFolder();
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folders.map(folder => ({ title: folder.title, path: Folders.relWsFolder(folder, wsFolder) })));
let folderDetails = folders.map(folder => ({
...folder,
path: Folders.relWsFolder(folder, wsFolder)
}));
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folderDetails, true);
// Reinitialize the folder listeners
PagesListener.startWatchers();
}
/**

View File

@@ -5,6 +5,9 @@ import { commands, env, Uri, ViewColumn, window } from "vscode";
import { Settings } from '../helpers';
import { PreviewSettings } from '../models';
import { format } from 'date-fns';
import { DateHelper } from '../helpers/DateHelper';
import { Article } from '.';
import { urlJoin } from 'url-join-ts';
export class Preview {
@@ -31,12 +34,25 @@ export class Preview {
const article = editor ? ArticleHelper.getFrontMatter(editor) : null;
let slug = article?.data ? article.data.slug : "";
if (settings.pathname) {
let pathname = settings.pathname;
if (article?.data) {
const contentType = ArticleHelper.getContentType(article.data);
if (contentType && contentType.previewPath) {
pathname = contentType.previewPath;
}
}
if (!slug) {
slug = Article.getSlug();
}
if (pathname) {
const articleDate = ArticleHelper.getDate(article);
try {
slug = join(format(articleDate || new Date(), settings.pathname), slug);
slug = join(format(articleDate || new Date(), DateHelper.formatUpdate(pathname) as string), slug);
} catch (error) {
slug = join(settings.pathname, slug);
slug = join(pathname, slug);
}
}
@@ -112,9 +128,9 @@ export class Preview {
</head>
<body>
<div class="slug">
<input type="text" value="${join(localhostUrl.toString(), slug)}" disabled />
<input type="text" value="${urlJoin(localhostUrl.toString(), slug || '')}" disabled />
</div>
<iframe src="${join(localhostUrl.toString(), slug)}" >
<iframe src="${urlJoin(localhostUrl.toString(), slug || '')}" >
</body>
</html>`;
}
@@ -131,4 +147,4 @@ export class Preview {
pathname
};
}
}
}

View File

@@ -5,6 +5,7 @@ import { Notifications } from "../helpers/Notifications";
import { Template } from "./Template";
import { Folders } from "./Folders";
import { Settings } from "../helpers";
import { SETTINGS_CONTENT_DEFAULT_FILETYPE } from "../constants";
export class Project {
@@ -27,6 +28,7 @@ categories: []
public static async init(sampleTemplate: boolean = true) {
try {
Settings.createTeamSettings();
const fileType = Settings.get<string>(SETTINGS_CONTENT_DEFAULT_FILETYPE);
const folder = Template.getSettings();
const templatePath = Project.templatePath();
@@ -35,7 +37,7 @@ categories: []
return;
}
const article = Uri.file(join(templatePath.fsPath, "article.md"));
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
if (!fs.existsSync(templatePath.fsPath)) {
await workspace.fs.createDirectory(templatePath);

View File

@@ -3,6 +3,7 @@ import * as vscode from 'vscode';
import { ArticleHelper, SeoHelper, Settings } from '../helpers';
import { ExplorerView } from '../explorerView/ExplorerView';
import { DefaultFields } from '../constants';
import { ContentType } from '../helpers/ContentType';
export class StatusListener {
@@ -15,6 +16,11 @@ export class StatusListener {
public static async verify(frontMatterSB: vscode.StatusBarItem, collection: vscode.DiagnosticCollection) {
const draftMsg = "in draft";
const publishMsg = "to publish";
const draft = ContentType.getDraftField();
if (!draft || draft.type !== "boolean") {
frontMatterSB.hide();
}
let editor = vscode.window.activeTextEditor;
if (editor && ArticleHelper.isMarkdownFile()) {

View File

@@ -2,15 +2,17 @@ import { Questions } from './../helpers/Questions';
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants';
import { format } from 'date-fns';
import sanitize from '../helpers/Sanitize';
import { SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants';
import { ArticleHelper, Settings } from '../helpers';
import { Article } from '.';
import { Notifications } from '../helpers/Notifications';
import { CONTEXT } from '../constants';
import { Project } from './Project';
import { Folders } from './Folders';
import { ContentType } from '../helpers/ContentType';
import { ContentType as IContentType } from '../models';
import { PagesListener } from '../listeners';
import { extname } from 'path';
export class Template {
@@ -49,6 +51,7 @@ export class Template {
public static async generate() {
const folder = Template.getSettings();
const editor = vscode.window.activeTextEditor;
const fileType = Settings.get<string>(SETTINGS_CONTENT_DEFAULT_FILETYPE);
if (folder && editor && ArticleHelper.isMarkdownFile()) {
const article = ArticleHelper.getFrontMatter(editor);
@@ -82,7 +85,7 @@ export class Template {
if (templatePath) {
let fileContents = ArticleHelper.stringifyFrontMatter(keepContents === "no" ? "" : clonedArticle.content, clonedArticle.data);
const templateFile = path.join(templatePath.fsPath, `${titleValue}.md`);
const templateFile = path.join(templatePath.fsPath, `${titleValue}.${fileType}`);
fs.writeFileSync(templateFile, fileContents, { encoding: "utf-8" });
Notifications.info(`Template created and is now available in your ${folder} folder.`);
@@ -95,7 +98,7 @@ export class Template {
*/
public static async create(folderPath: string) {
const folder = Settings.get<string>(SETTING_TEMPLATES_FOLDER);
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const contentTypes = ContentType.getAll();
if (!folderPath) {
Notifications.warning(`Incorrect project folder path retrieved.`);
@@ -133,16 +136,15 @@ export class Template {
return;
}
const fileExt = path.parse(selectedTemplate).ext;
const sanitizedName = sanitize(titleValue.toLowerCase().replace(/ /g, "-"));
let newFileName = `${sanitizedName}${fileExt}`;
if (prefix && typeof prefix === "string") {
newFileName = `${format(new Date(), prefix)}-${newFileName}`;
const templateData = ArticleHelper.getFrontMatterByPath(template.fsPath);
let contentType: IContentType | undefined;
if (templateData && templateData.data && templateData.data.type) {
contentType = contentTypes?.find(t => t.name === templateData.data.type);
}
const newFilePath = path.join(folderPath, newFileName);
if (fs.existsSync(newFilePath)) {
Notifications.warning(`File already exists, please remove it before creating a new one with the same title.`);
const fileExtension = extname(template.fsPath).replace(".", "");
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue, fileExtension);
if (!newFilePath) {
return;
}
@@ -162,7 +164,7 @@ export class Template {
fmData.title = titleValue;
}
if (typeof fmData.slug !== "undefined") {
fmData.slug = sanitizedName;
fmData.slug = ArticleHelper.sanitize(titleValue);
}
frontMatter = Article.updateDate(frontMatter);
@@ -178,6 +180,9 @@ export class Template {
}
Notifications.info(`Your new content has been created.`);
// Trigger a refresh for the dashboard
PagesListener.refresh();
}
/**

230
src/commands/Wysiwyg.ts Normal file
View File

@@ -0,0 +1,230 @@
import { commands, window, Selection, QuickPickItem } from "vscode";
import { COMMAND_NAME, CONTEXT, SETTINGS_CONTENT_WYSIWYG } from "../constants";
import { Settings } from "../helpers";
enum MarkupType {
bold = 1,
italic,
strikethrough,
code,
codeblock,
blockquote,
heading,
unorderedList,
orderedList,
taskList
}
export class Wysiwyg {
/**
* Registers the markup commands for the WYSIWYG controls
* @param subscriptions
* @returns
*/
public static async registerCommands(subscriptions: any) {
const wysiwygEnabled = Settings.get(SETTINGS_CONTENT_WYSIWYG);
if (!wysiwygEnabled) {
return;
}
await commands.executeCommand('setContext', CONTEXT.wysiwyg, true);
// Surrounding markup
subscriptions.push(commands.registerCommand(COMMAND_NAME.bold, () => this.addMarkup(MarkupType.bold)));
subscriptions.push(commands.registerCommand(COMMAND_NAME.italic, () => this.addMarkup(MarkupType.italic)));
subscriptions.push(commands.registerCommand(COMMAND_NAME.strikethrough, () => this.addMarkup(MarkupType.strikethrough)));
subscriptions.push(commands.registerCommand(COMMAND_NAME.code, () => this.addMarkup(MarkupType.code)));
subscriptions.push(commands.registerCommand(COMMAND_NAME.codeblock, () => this.addMarkup(MarkupType.codeblock)));
// Prefix markup
subscriptions.push(commands.registerCommand(COMMAND_NAME.heading, () => this.addMarkup(MarkupType.heading)));
subscriptions.push(commands.registerCommand(COMMAND_NAME.blockquote, () => this.addMarkup(MarkupType.blockquote)));
subscriptions.push(commands.registerCommand(COMMAND_NAME.unorderedlist, () => this.addMarkup(MarkupType.unorderedList)));
subscriptions.push(commands.registerCommand(COMMAND_NAME.orderedlist, () => this.addMarkup(MarkupType.orderedList)));
subscriptions.push(commands.registerCommand(COMMAND_NAME.taskList, () => this.addMarkup(MarkupType.taskList)));
// Options
subscriptions.push(commands.registerCommand(COMMAND_NAME.options, async () => {
const qpItems: QuickPickItem[] = [
{ label: "$(list-unordered) Unordered list", detail: "Add an unordered list", alwaysShow: true, },
{ label: "$(list-ordered) Ordered list", detail: "Add an ordered list", alwaysShow: true },
{ label: "$(tasklist) Task list", detail: "Add a task list", alwaysShow: true },
{ label: "$(code) Code", detail: "Add inline code snippet", alwaysShow: true },
{ label: "$(symbol-namespace) Code block", detail: "Add a code block", alwaysShow: true },
]
const option = await window.showQuickPick([ ...qpItems ], {
placeHolder: "Which type of markup would you like to insert?",
canPickMany: false,
ignoreFocusOut: false,
});
if (option) {
if (option.label === qpItems[0].label) {
await this.addMarkup(MarkupType.unorderedList);
} else if (option.label === qpItems[1].label) {
await this.addMarkup(MarkupType.orderedList);
} else if (option.label === qpItems[2].label) {
await this.addMarkup(MarkupType.taskList);
} else if (option.label === qpItems[3].label) {
await this.addMarkup(MarkupType.code);
} else if (option.label === qpItems[4].label) {
await this.addMarkup(MarkupType.codeblock);
}
}
}));
}
/**
* Add the markup to the content
* @param type
* @returns
*/
private static async addMarkup(type: MarkupType) {
const editor = window.activeTextEditor;
if (!editor) {
return;
}
const selection = editor.selection;
const hasTextSelection = !selection.isEmpty;
const markers = this.getMarkers(type);
if (!markers) {
return;
}
const crntSelection = selection.active;
if (hasTextSelection) {
// Replace the selection and surround with the markup
const selectionText = editor.document.getText(selection);
const txt = await this.insertText(markers, type, selectionText);
editor.edit(builder => {
builder.replace(selection, txt);
});
} else {
const txt = await this.insertText(markers, type);
// Insert the markers where cursor is located.
const markerLength = this.isMarkupWrapping(type) ? txt.length + 1 : markers.length;
let newPosition = crntSelection.with(crntSelection.line, crntSelection.character + markerLength);
await editor.edit(builder => {
builder.insert(newPosition, txt);
});
if (type === MarkupType.codeblock) {
newPosition = crntSelection.with(crntSelection.line + 1, 0);
}
editor.selection = new Selection(newPosition, newPosition);
}
}
/**
* Check if the text will be wrapped
* @param type
* @returns
*/
private static isMarkupWrapping(type: MarkupType) {
return (
type === MarkupType.blockquote ||
type === MarkupType.heading ||
type === MarkupType.unorderedList ||
type === MarkupType.orderedList ||
type === MarkupType.taskList
);
}
/**
* Insert text at the current cursor position
*/
private static async insertText(marker: string | undefined, type: MarkupType, text: string | null = null) {
const crntText = text || this.lineBreak(type);
if (this.isMarkupWrapping(type)) {
if (type === MarkupType.heading) {
const headingLvl = await window.showQuickPick([
"Heading 1",
"Heading 2",
"Heading 3",
"Heading 4",
"Heading 5",
"Heading 6"
], {
canPickMany: false,
placeHolder: "Which heading level do you want to insert?",
ignoreFocusOut: false
});
if (headingLvl) {
const headingNr = parseInt(headingLvl.replace("Heading ", ""));
return `${Array(headingNr + 1).join(marker)} ${crntText}`;
}
}
if (type === MarkupType.unorderedList || type === MarkupType.taskList) {
const lines = crntText.split("\n").map(line => `${marker} ${line}`);
return lines.join("\n");
}
if (type === MarkupType.orderedList) {
const lines = crntText.split("\n").map((line, idx) => `${idx+1}. ${line}`);
return lines.join("\n");
}
return `${marker} ${crntText}`;
} else {
return `${marker}${crntText}${marker}`;
}
}
/**
* Check if linebreak needs to be added
* @param type
* @returns
*/
private static lineBreak(type: MarkupType) {
if (type === MarkupType.codeblock) {
return `\n\n`;
}
return "";
}
/**
* Retrieve the type of markers
* @param type
* @returns
*/
private static getMarkers(type: MarkupType) {
switch(type) {
case MarkupType.bold:
return `**`;
case MarkupType.italic:
return `*`;
case MarkupType.strikethrough:
return `~~`;
case MarkupType.code:
return "`";
case MarkupType.codeblock:
return "```";
case MarkupType.blockquote:
return ">";
case MarkupType.heading:
return "#";
case MarkupType.unorderedList:
return "-";
case MarkupType.orderedList:
return "1.";
case MarkupType.taskList:
return "- [ ]";
default:
return;
}
}
}

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
export interface ICompressIconProps {
className?: string;
}
export const CompressIcon: React.FunctionComponent<ICompressIconProps> = ({className}: React.PropsWithChildren<ICompressIconProps>) => {
return (
<svg className={className || ""} aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path fill="currentColor" d="M436 192H312c-13.3 0-24-10.7-24-24V44c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v84h84c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12zm-276-24V44c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v84H12c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h124c13.3 0 24-10.7 24-24zm0 300V344c0-13.3-10.7-24-24-24H12c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h84v84c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm192 0v-84h84c6.6 0 12-5.4 12-12v-40c0-6.6-5.4-12-12-12H312c-13.3 0-24 10.7-24 24v124c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12z"></path>
</svg>
);
};

View File

@@ -4,6 +4,8 @@ export const DEFAULT_CONTENT_TYPE_NAME = 'default';
export const DEFAULT_CONTENT_TYPE: ContentType = {
"name": "default",
"pageBundle": false,
"previewPath": null,
"fields": [
{
"title": "Title",
@@ -21,14 +23,14 @@ export const DEFAULT_CONTENT_TYPE: ContentType = {
"type": "datetime"
},
{
"title": "Article preview",
"title": "Content preview",
"name": "preview",
"type": "image"
},
{
"title": "Is in draft",
"name": "draft",
"type": "boolean"
"type": "draft"
},
{
"title": "Tags",

View File

@@ -2,5 +2,6 @@
export const DefaultFields = {
PublishingDate: `date`,
LastModified: `lastmod`,
Description: `description`
Description: `description`,
Slug: `slug`
};

View File

@@ -28,7 +28,23 @@ export const COMMAND_NAME = {
collapseSections: getCommandName("collapseSections"),
preview: getCommandName("preview"),
dashboard: getCommandName("dashboard"),
dashboardMedia: getCommandName("dashboard.media"),
dashboardClose: getCommandName("dashboard.close"),
promote: getCommandName("promoteSettings"),
insertImage: getCommandName("insertImage"),
createFolder: getCommandName("createFolder"),
diagnostics: getCommandName("diagnostics"),
// WYSIWYG
bold: getCommandName("markup.bold"),
italic: getCommandName("markup.italic"),
strikethrough: getCommandName("markup.strikethrough"),
code: getCommandName("markup.code"),
codeblock: getCommandName("markup.codeblock"),
heading: getCommandName("markup.heading"),
blockquote: getCommandName("markup.blockquote"),
unorderedlist: getCommandName("markup.unorderedlist"),
orderedlist: getCommandName("markup.orderedlist"),
taskList: getCommandName("markup.tasklist"),
options: getCommandName("markup.options"),
};

View File

@@ -5,4 +5,13 @@ export const ExtensionState = {
Version: `frontMatter:Version`,
SettingPromoted: `frontMatter:Settings:Promoted`,
MoveTemplatesFolder: `frontMatter:Templates:Move`,
Dashboard: {
Contents: {
Sorting: `frontMatter:Dashboard:Contents:Sorting`,
},
Media: {
Sorting: `frontMatter:Dashboard:Media:Sorting`,
}
}
};

View File

@@ -0,0 +1,21 @@
export const FrameworkDetectors = [
{
"framework": {"name": "gatsby", "dist": "public", "static": "static", "build": "gatsby build"},
"requiredFiles": ["gatsby-config.js"],
"requiredDependencies": ["gatsby"]
},
{
"framework": {"name": "hugo", "dist": "public", "static": "static", "build": "hugo"},
"requiredFiles": ["config.toml", "config.yaml", "config.yml"]
},
{
"framework": {"name": "next", "dist": ".next", "static": "public", "build": "next build"},
"requiredFiles": ["next.config.js"],
"requiredDependencies": ["next"]
},
{
"framework": {"name": "nuxt", "dist": "dist", "static": "static", "build": "nuxt"},
"requiredFiles": ["nuxt.config.js"],
"requiredDependencies": ["nuxt"]
}
];

View File

@@ -2,5 +2,7 @@ export const CONTEXT = {
canInit: "frontMatterCanInit",
canOpenPreview: "frontMatterCanOpenPreview",
canOpenDashboard: "frontMatterCanOpenDashboard",
isEnabled: "frontMatter:enabled"
isEnabled: "frontMatter:enabled",
isDashboardOpen: "frontMatter:dashboard:open",
wysiwyg: "frontMatter:markdown:wysiwyg",
};

View File

@@ -2,8 +2,12 @@ export const EXTENSION_NAME = "Front Matter";
export const CONFIG_KEY = "frontMatter";
export const SETTING_GLOBAL_NOTIFICATIONS = "global.notifications";
export const SETTING_TAXONOMY_TAGS = "taxonomy.tags";
export const SETTING_TAXONOMY_CATEGORIES = "taxonomy.categories";
export const SETTING_TAXONOMY_CUSTOM = "taxonomy.customTaxonomy";
export const SETTING_DATE_FORMAT = "taxonomy.dateFormat";
export const SETTING_COMMA_SEPARATED_FIELDS = "taxonomy.commaSeparatedFields";
export const SETTING_TAXONOMY_CONTENT_TYPES = "taxonomy.contentTypes";
@@ -20,6 +24,7 @@ export const SETTING_REMOVE_QUOTES = "taxonomy.noPropertyValueQuotes";
export const SETTING_FRONTMATTER_TYPE = "taxonomy.frontMatterType";
export const SETTING_SEO_TITLE_LENGTH = "taxonomy.seoTitleLength";
export const SETTING_SEO_SLUG_LENGTH = "taxonomy.seoSlugLength";
export const SETTING_SEO_DESCRIPTION_LENGTH = "taxonomy.seoDescriptionLength";
export const SETTING_SEO_CONTENT_MIN_LENGTH = "taxonomy.seoContentLengh";
export const SETTING_SEO_DESCRIPTION_FIELD = "taxonomy.seoDescriptionField";
@@ -38,10 +43,22 @@ export const SETTING_AUTO_UPDATE_DATE = "content.autoUpdateDate";
export const SETTINGS_CONTENT_PAGE_FOLDERS = "content.pageFolders";
export const SETTINGS_CONTENT_STATIC_FOLDER = "content.publicFolder";
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
export const SETTINGS_CONTENT_DRAFT_FIELD = "content.draftField";
export const SETTINGS_CONTENT_SORTING = "content.sorting";
export const SETTINGS_CONTENT_WYSIWYG = "content.wysiwyg";
export const SETTINGS_CONTENT_SORTING_DEFAULT = "content.defaultSorting";
export const SETTINGS_MEDIA_SORTING_DEFAULT = "content.defaultSorting";
export const SETTINGS_CONTENT_DEFAULT_FILETYPE = "content.defaultFileType";
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
export const SETTINGS_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
export const SETTINGS_FRAMEWORK_ID = "framework.id";
export const SETTING_SITE_BASEURL = "site.baseURL";
/**
* @deprecated
*/

View File

@@ -3,5 +3,6 @@ export enum DashboardCommand {
pages = "pages",
settings = "settings",
media = "media",
viewData = "viewData"
viewData = "viewData",
mediaUpdate = "mediaUpdate"
}

View File

@@ -17,5 +17,8 @@ export enum DashboardMessage {
deleteMedia = 'deleteMedia',
insertPreviewImage = 'insertPreviewImage',
updateMediaMetadata = 'updateMediaMetadata',
createMediaFolder = 'createMediaFolder'
createMediaFolder = 'createMediaFolder',
setFramework = 'setFramework',
setState = 'setState',
runCustomScript = 'runCustomScript',
}

View File

@@ -1,5 +1,5 @@
import { Menu } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/outline';
import {ChevronDownIcon} from '@heroicons/react/outline';
import * as React from 'react';
import { MenuItem, MenuItems } from './Menu';
@@ -28,7 +28,7 @@ export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onCli
<Menu as="span" className="-ml-px relative block">
<Menu.Button
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-700 hover:bg-teal-800 focus:outline-none disabled:bg-gray-500"
className="h-full inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-700 hover:bg-teal-800 focus:outline-none disabled:bg-gray-500"
disabled={disabled}>
<span className="sr-only">Open options</span>
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />

View File

@@ -6,6 +6,7 @@ import { Header } from '../Header';
import { Overview } from './Overview';
import { Spinner } from '../Spinner';
import { SponsorMsg } from '../SponsorMsg';
import usePages from '../../hooks/usePages';
export interface IContentsProps {
pages: Page[];
@@ -14,23 +15,22 @@ export interface IContentsProps {
export const Contents: React.FunctionComponent<IContentsProps> = ({pages, loading}: React.PropsWithChildren<IContentsProps>) => {
const settings = useRecoilValue(SettingsSelector);
const { pageItems } = usePages(pages);
const pageFolders = [...new Set(pages.map(page => page.fmFolder))];
const pageFolders = [...new Set(pageItems.map(page => page.fmFolder))];
return (
<main className={`h-full w-full`}>
<div className="flex flex-col h-full overflow-auto">
<Header
folders={pageFolders}
totalPages={pages.length}
settings={settings} />
<div className="flex flex-col h-full overflow-auto">
<Header
folders={pageFolders}
totalPages={pageItems.length}
settings={settings} />
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4">
{ loading ? <Spinner /> : <Overview pages={pages} settings={settings} /> }
</div>
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4">
{ loading ? <Spinner /> : <Overview pages={pageItems} settings={settings} /> }
</div>
</main>
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
</div>
);
};

View File

@@ -3,11 +3,12 @@ import { useRecoilValue } from 'recoil';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { DashboardMessage } from '../../DashboardMessage';
import { Page } from '../../models/Page';
import { SettingsSelector, ViewSelector, ViewType } from '../../state';
import { SettingsSelector, ViewSelector } from '../../state';
import { DateField } from '../DateField';
import { Status } from '../Status';
import { Messenger } from '@estruyf/vscode/dist/client';
import useContentType from '../../../hooks/useContentType';
import { DashboardViewType } from '../../models';
export interface IItemProps extends Page {}
@@ -22,10 +23,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
Messenger.send(DashboardMessage.openFile, fmFilePath);
};
if (view === ViewType.Grid) {
if (view === DashboardViewType.Grid) {
return (
<li className="relative">
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md hover:shadow-xl dark:hover:bg-vulcan-100`}
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50`}
onClick={openFile}>
<div className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200">
{
@@ -41,7 +42,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
<div className="p-4 w-full">
<div className={`flex justify-between items-center`}>
<Status draft={!!draft} />
<Status draft={draft} />
<DateField value={date} />
</div>
@@ -53,7 +54,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
</button>
</li>
);
} else if (view === ViewType.List) {
} else if (view === DashboardViewType.List) {
return (
<li className="relative">
<button 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 border-gray-300 hover:bg-gray-200 dark:border-vulcan-50 dark:hover:bg-vulcan-50 hover:bg-opacity-70`} onClick={openFile}>
@@ -64,7 +65,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
<DateField value={date} />
</div>
<div className="col-span-2">
<Status draft={!!draft} />
<Status draft={draft} />
</div>
</button>
</li>

View File

@@ -1,6 +1,7 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { ViewSelector, ViewType } from '../../state';
import { DashboardViewType } from '../../models';
import { ViewSelector } from '../../state';
export interface IListProps {}
@@ -8,15 +9,15 @@ export const List: React.FunctionComponent<IListProps> = ({children}: React.Prop
const view = useRecoilValue(ViewSelector);
let className = '';
if (view === ViewType.Grid) {
if (view === DashboardViewType.Grid) {
className = `grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8`;
} else if (view === ViewType.List) {
} else if (view === DashboardViewType.List) {
className = `-mx-4`;
}
return (
<ul role="list" className={className}>
{view === ViewType.List && (
{view === DashboardViewType.List && (
<li className="px-5 relative uppercase text-vulcan-100 dark:text-whisper-900 py-2 border-b border-vulcan-50">
<div className={`grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8`}>
<div className="col-span-8">

View File

@@ -1,5 +1,5 @@
import { Disclosure } from '@headlessui/react';
import { ChevronRightIcon } from '@heroicons/react/solid';
import {ChevronRightIcon} from '@heroicons/react/solid';
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { groupBy } from '../../../helpers/GroupBy';

View File

@@ -2,12 +2,12 @@ import * as React from 'react';
import { Spinner } from './Spinner';
import useMessages from '../hooks/useMessages';
import useDarkMode from '../../hooks/useDarkMode';
import usePages from '../hooks/usePages';
import { WelcomeScreen } from './WelcomeScreen';
import { useRecoilValue } from 'recoil';
import { DashboardViewSelector } from '../state';
import { Contents } from './Contents/Contents';
import { Media } from './Media/Media';
import { NavigationType } from '../models';
export interface IDashboardProps {
showWelcome: boolean;
@@ -15,7 +15,6 @@ export interface IDashboardProps {
export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome}: React.PropsWithChildren<IDashboardProps>) => {
const { loading, pages, settings } = useMessages();
const { pageItems } = usePages(pages);
const view = useRecoilValue(DashboardViewSelector);
useDarkMode();
@@ -31,9 +30,15 @@ export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome
return <WelcomeScreen settings={settings} />;
}
if (view === 'media') {
return <Media />;
}
return <Contents pages={pageItems} loading={loading} />;
return (
<main className={`h-full w-full`}>
{
view === NavigationType.Media ? (
<Media />
) : (
<Contents pages={pages} loading={loading} />
)
}
</main>
);
};

View File

@@ -1,5 +1,6 @@
import { format, parseJSON } from 'date-fns';
import { format } from 'date-fns';
import * as React from 'react';
import { DateHelper } from '../../helpers/DateHelper';
export interface IDateFieldProps {
value: Date | string;
@@ -10,9 +11,9 @@ export const DateField: React.FunctionComponent<IDateFieldProps> = ({value}: Rea
React.useEffect(() => {
try {
const parsedValue = typeof value === 'string' ? parseJSON(value) : value;
const dateString = format(parsedValue, 'yyyy-MM-dd');
setDateValue(dateString);
const parsedValue = typeof value === 'string' ? DateHelper.tryParse(value) : value;
const dateString = parsedValue ? format(parsedValue, 'yyyy-MM-dd') : parsedValue;
setDateValue(dateString || "");
} catch (e) {
// Date is invalid
}

View File

@@ -4,23 +4,29 @@ import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { HOME_PAGE_NAVIGATION_ID } from '../../../constants';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { SelectedMediaFolderAtom, SettingsAtom } from '../../state';
import { SearchAtom, SelectedMediaFolderAtom, SettingsAtom } from '../../state';
export interface IBreadcrumbProps {}
export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: React.PropsWithChildren<IBreadcrumbProps>) => {
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const settings = useRecoilValue(SettingsAtom);
const [ , setSearchValue ] = useRecoilState(SearchAtom);
const [ folders, setFolders ] = React.useState<string[]>([]);
const settings = useRecoilValue(SettingsAtom);
if (!settings?.wsFolder) {
return null;
const updateFolder = (folder: string) => {
setSearchValue('');
setSelectedFolder(folder);
}
React.useEffect(() => {
if (!settings) {
return;
}
const { wsFolder, staticFolder, contentFolders } = settings;
const isValid = (folderPath: string) => {
const isValid = (folderPath: string) => {
if (staticFolder) {
const staticPath = parseWinPath(join(wsFolder, staticFolder)) as string;
const relPath = folderPath.replace(staticPath, '') as string;
@@ -33,7 +39,8 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: Rea
}
for (let i = 0; i < contentFolders.length; i++) {
const contentFolder = parseWinPath(contentFolders[i]) as string;
const folder = contentFolders[i];
const contentFolder = parseWinPath(folder.path) as string;
const relContentPath = folderPath.replace(contentFolder, '');
return relContentPath.length > 1 && folderPath.startsWith(contentFolder);
}
@@ -59,41 +66,40 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: Rea
setFolders(allFolders);
}
}, [selectedFolder]);
}, [selectedFolder, settings]);
return (
<nav className="bg-gray-200 text-vulcan-300 dark:bg-vulcan-400 dark:text-whisper-600 border-b border-gray-300 dark:border-vulcan-100 flex py-2" aria-label="Breadcrumb">
<ol role="list" className="w-full mx-auto flex space-x-4 px-5">
<li className="flex">
<ol role="list" className="flex space-x-4 px-5 flex-1">
<li className="flex">
<div className="flex items-center">
<button onClick={() => setSelectedFolder(HOME_PAGE_NAVIGATION_ID)} className="text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500">
<CollectionIcon className="flex-shrink-0 h-5 w-5" aria-hidden="true" />
<span className="sr-only">Home</span>
</button>
</div>
</li>
{folders.map((folder) => (
<li key={folder} className="flex">
<div className="flex items-center">
<button onClick={() => setSelectedFolder(HOME_PAGE_NAVIGATION_ID)} className="text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500">
<CollectionIcon className="flex-shrink-0 h-5 w-5" aria-hidden="true" />
<span className="sr-only">Home</span>
<svg
className="flex-shrink-0 h-5 w-5 text-gray-300 dark:text-whisper-900"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
</svg>
<button
onClick={() => updateFolder(folder)}
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500"
>
{basename(folder)}
</button>
</div>
</li>
{folders.map((folder) => (
<li key={folder} className="flex">
<div className="flex items-center">
<svg
className="flex-shrink-0 h-5 w-5 text-gray-300 dark:text-whisper-900"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
</svg>
<button
onClick={() => setSelectedFolder(folder)}
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500"
>
{basename(folder)}
</button>
</div>
</li>
))}
</ol>
</nav>
))}
</ol>
);
};

View File

@@ -1,7 +1,7 @@
import { XCircleIcon } from '@heroicons/react/solid';
import {XCircleIcon} from '@heroicons/react/solid';
import * as React from 'react';
import { useRecoilValue, useResetRecoilState } from 'recoil';
import { SortingSelector, FolderSelector, TagSelector, CategorySelector, SortingAtom, DEFAULT_SORTING_OPTION, FolderAtom, DEFAULT_FOLDER_STATE, TagAtom, CategoryAtom, DEFAULT_TAG_STATE, DEFAULT_CATEGORY_STATE } from '../../state';
import { FolderSelector, TagSelector, CategorySelector, SortingAtom, FolderAtom, DEFAULT_FOLDER_STATE, TagAtom, CategoryAtom, DEFAULT_TAG_STATE, DEFAULT_CATEGORY_STATE } from '../../state';
import { DefaultValue } from 'recoil';
@@ -17,7 +17,6 @@ export interface IClearFiltersProps {}
export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (props: React.PropsWithChildren<IClearFiltersProps>) => {
const [ show, setShow ] = React.useState(false);
const sorting = useRecoilValue(SortingSelector);
const folder = useRecoilValue(FolderSelector);
const tag = useRecoilValue(TagSelector);
const category = useRecoilValue(CategorySelector);
@@ -36,19 +35,19 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (props:
};
React.useEffect(() => {
if (sorting !== DEFAULT_SORTING_OPTION || folder !== DEFAULT_FOLDER_STATE || tag !== DEFAULT_TAG_STATE || category !== DEFAULT_CATEGORY_STATE) {
if (folder !== DEFAULT_FOLDER_STATE || tag !== DEFAULT_TAG_STATE || category !== DEFAULT_CATEGORY_STATE) {
setShow(true);
} else {
setShow(false);
}
}, [sorting, folder, tag, category]);
}, [folder, tag, category]);
if (!show) return null;
return (
<button className="flex items-center hover:text-teal-600" onClick={reset} title={`Clear filters, grouping, and sorting`}>
<XCircleIcon className={`inline-block w-5 h-5 mr-1`} /><span>Clear</span>
<span className={`sr-only`}> filters, grouping, and sorting</span>
<span className={`sr-only`}> filters and grouping</span>
</button>
);
};

View File

@@ -1,5 +1,5 @@
import { Menu } from '@headlessui/react';
import { FilterIcon } from '@heroicons/react/solid';
import {FilterIcon} from '@heroicons/react/solid';
import * as React from 'react';
import { MenuButton, MenuItem, MenuItems } from '../Menu';

View File

@@ -1,19 +1,19 @@
import { Menu } from '@headlessui/react';
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { FolderAtom } from '../../state';
import { useRecoilState, useRecoilValue } from 'recoil';
import { FolderAtom, SettingsSelector } from '../../state';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
export interface IFoldersProps {
folders: string[];
}
export interface IFoldersProps {}
const DEFAULT_TYPE = "All types";
export const Folders: React.FunctionComponent<IFoldersProps> = ({folders}: React.PropsWithChildren<IFoldersProps>) => {
export const Folders: React.FunctionComponent<IFoldersProps> = ({}: React.PropsWithChildren<IFoldersProps>) => {
const [ crntFolder, setCrntFolder ] = useRecoilState(FolderAtom);
const settings = useRecoilValue(SettingsSelector);
const contentFolders = settings?.contentFolders || [];
if (folders.length <= 1) {
if (contentFolders.length <= 1) {
return null;
}
@@ -29,12 +29,12 @@ export const Folders: React.FunctionComponent<IFoldersProps> = ({folders}: React
isCurrent={!crntFolder}
onClick={(value) => setCrntFolder(value)} />
{folders.map((option) => (
{contentFolders.map((option) => (
<MenuItem
key={option}
title={option}
value={option}
isCurrent={option === crntFolder}
key={option.title}
title={option.title}
value={option.title}
isCurrent={option.title === crntFolder}
onClick={(value) => setCrntFolder(value)} />
))}
</MenuItems>

View File

@@ -3,21 +3,21 @@ import { Sorting } from './Sorting';
import { Searchbox } from './Searchbox';
import { Filter } from './Filter';
import { Folders } from './Folders';
import { Settings } from '../../models';
import { Settings, NavigationType } from '../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { Startup } from '../Startup';
import { Navigation } from '../Navigation';
import { Grouping } from '.';
import { ViewSwitch } from './ViewSwitch';
import { useRecoilState } from 'recoil';
import { CategoryAtom, DashboardViewAtom, TagAtom } from '../../state';
import { useRecoilState, useResetRecoilState } from 'recoil';
import { CategoryAtom, DashboardViewAtom, SortingAtom, TagAtom } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { ClearFilters } from './ClearFilters';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { PhotographIcon } from '@heroicons/react/outline';
import { Pagination } from '../Media/Pagination';
import {PhotographIcon} from '@heroicons/react/outline';
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
import { ChoiceButton } from '../ChoiceButton';
import { Breadcrumb } from './Breadcrumb';
import { MediaHeaderBottom } from '../Media/MediaHeaderBottom';
export interface IHeaderProps {
settings: Settings | null;
@@ -33,6 +33,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
const [ crntTag, setCrntTag ] = useRecoilState(TagAtom);
const [ crntCategory, setCrntCategory ] = useRecoilState(CategoryAtom);
const [ view, setView ] = useRecoilState(DashboardViewAtom);
const resetSorting = useResetRecoilState(SortingAtom)
const createContent = () => {
Messenger.send(DashboardMessage.createContent);
@@ -46,22 +47,31 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
Messenger.send(DashboardMessage.createByTemplate);
};
const updateView = (view: NavigationType) => {
setView(view);
resetSorting();
}
return (
<div className={`w-full sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
<div className={`px-4 bg-gray-50 dark:bg-vulcan-50 border-b-2 border-gray-200 dark:border-vulcan-200`}>
<div className={`flex items-center justify-start`}>
<button className={`p-2 flex items-center ${view === "contents" ? "bg-gray-200 dark:bg-vulcan-200" : ""} hover:bg-gray-100 dark:hover:bg-vulcan-100`} onClick={() => setView("contents")}>
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
</button>
<button className={`p-2 flex items-center ${view === "media" ? "bg-gray-200 dark:bg-vulcan-200" : ""} hover:bg-gray-100 dark:hover:bg-vulcan-100`} onClick={() => setView("media")}>
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
</button>
</div>
<div className="mb-0 border-b bg-gray-100 dark:bg-vulcan-500 border-gray-200 dark:border-vulcan-300 h-12">
<ul className="flex items-center justify-start h-full -mb-px" data-tabs-toggle="#myTabContent" role="tablist">
<li className="mr-2" role="presentation">
<button className={`flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === NavigationType.Contents ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`} type="button" role="tab" aria-controls="profile" aria-selected="false" onClick={() => updateView(NavigationType.Contents)}>
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
</button>
</li>
<li className="mr-2" role="presentation">
<button className={`flex items-center py-2 px-4 text-sm font-medium text-center text-gray-500 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === NavigationType.Media ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`} type="button" role="tab" aria-controls="dashboard" aria-selected="true" onClick={() => updateView(NavigationType.Media)}>
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
</button>
</li>
</ul>
</div>
{
view === "contents" && (
view === NavigationType.Contents && (
<>
<div className={`px-4 mt-3 mb-2 flex items-center justify-between`}>
<Searchbox />
@@ -95,10 +105,10 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
</div>
</div>
<div className={`py-4 px-5 w-full flex items-center justify-between lg:justify-end space-x-4 lg:space-x-6 xl:space-x-8 bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100`}>
<div className={`py-4 px-5 w-full flex items-center justify-between lg:justify-end bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100 space-x-4 lg:space-x-6 xl:space-x-8`}>
<ClearFilters />
<Folders folders={folders || []} />
<Folders />
<Filter label={`Tag`} activeItem={crntTag} items={settings?.tags || []} onClick={(value) => setCrntTag(value)} />
@@ -106,17 +116,18 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
<Grouping />
<Sorting />
<Sorting view={NavigationType.Contents} />
</div>
</>
)
}
{
view === "media" && (
view === NavigationType.Media && (
<>
<Pagination />
<Breadcrumb />
<MediaHeaderTop />
<MediaHeaderBottom />
</>
)
}

View File

@@ -0,0 +1,72 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { LIMIT } from '../../hooks/useMedia';
import { MediaTotalSelector, PageAtom } from '../../state';
import { PaginationButton } from './PaginationButton';
export interface IPaginationProps {}
export const Pagination: React.FunctionComponent<IPaginationProps> = (props: React.PropsWithChildren<IPaginationProps>) => {
const [ page, setPage ] = useRecoilState(PageAtom);
const totalMedia = useRecoilValue(MediaTotalSelector);
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
const getButtons = (): number[] => {
const maxButtons = 5;
const buttons: number[] = [];
const start = page - maxButtons;
const end = page + maxButtons;
for (let i = start; i <= end; i++) {
if (i >= 0 && i <= totalPages) {
buttons.push(i);
}
}
return buttons;
};
return (
<div className="flex justify-between items-center sm:justify-end space-x-2 text-sm">
<PaginationButton
title="First"
disabled={page === 0}
onClick={() => {
if (page > 0) {
setPage(0)
}
}} />
<PaginationButton
title="Previous"
disabled={page === 0}
onClick={() => {
if (page > 0) {
setPage(page - 1)
}
}} />
{getButtons().map((button) => (
<button
key={button}
disabled={button === page}
onClick={() => {
setPage(button)
}
}
className={`${page === button ? 'bg-gray-200 px-2 text-vulcan-500' : 'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500'} max-h-8`}
>{button + 1}</button>
))}
<PaginationButton
title="Next"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)} />
<PaginationButton
title="Last"
disabled={page >= totalPages}
onClick={() => setPage(totalPages)} />
</div>
);
};

View File

@@ -0,0 +1,46 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaTotalSelector, PageAtom, SearchAtom, SelectedMediaFolderSelector } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { RefreshIcon } from '@heroicons/react/outline';
import { LIMIT } from '../../hooks/useMedia';
export interface IPaginationStatusProps {}
export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> = (props: React.PropsWithChildren<IPaginationStatusProps>) => {
const totalMedia = useRecoilValue(MediaTotalSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const [ page, setPage ] = useRecoilState(PageAtom);
const [ , setSearch ] = useRecoilState(SearchAtom);
const getTotalPage = () => {
const mediaItems = ((page + 1) * LIMIT);
if (totalMedia < mediaItems) {
return totalMedia;
}
return mediaItems;
};
const refresh = () => {
setPage(0);
setSearch('');
Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder });
}
return (
<div className="hidden sm:flex">
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
title="Refresh media"
onClick={refresh}>
<RefreshIcon className={`h-5 w-5`} />
<span className="sr-only">Refresh media</span>
</button>
<p className="text-sm text-gray-500 dark:text-whisper-900">
Showing <span className="font-medium">{(page * LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
<span className="font-medium">{totalMedia}</span> results
</p>
</div>
);
};

View File

@@ -1,37 +1,46 @@
import { FilterIcon, SearchIcon } from '@heroicons/react/solid';
import {SearchIcon} from '@heroicons/react/solid';
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { useDebounce } from '../../../hooks/useDebounce';
import { SearchAtom } from '../../state';
export interface ISearchboxProps {}
export interface ISearchboxProps {
placeholder?: string;
}
export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({}: React.PropsWithChildren<ISearchboxProps>) => {
export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({placeholder}: React.PropsWithChildren<ISearchboxProps>) => {
const [ value, setValue ] = React.useState('');
const [ , setDebounceValue ] = useRecoilState(SearchAtom);
const [ debounceSearchValue, setDebounceValue ] = useRecoilState(SearchAtom);
const debounceSearch = useDebounce<string>(value, 500);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};
React.useEffect(() => {
if (!debounceSearchValue && value) {
setValue('');
}
} , [debounceSearchValue]);
React.useEffect(() => {
setDebounceValue(debounceSearch);
}, [debounceSearch]);
return (
<div className="flex space-x-4">
<div className="flex-1 min-w-0">
<div className="flex space-x-4 flex-1">
<div className="min-w-0">
<label htmlFor="search" className="sr-only">Search</label>
<div className="relative flex justify-center">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="search"
name="search"
className={`block w-full py-2 pl-10 pr-3 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none`}
placeholder="Search"
placeholder={placeholder || "Search"}
value={value}
onChange={handleChange}
/>

View File

@@ -1,37 +1,85 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { ExtensionState } from '../../../constants';
import { SortOrder, SortType } from '../../../models';
import { SortOption } from '../../constants/SortOption';
import { SearchSelector, SortingAtom } from '../../state';
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';
export interface ISortingProps {}
export interface ISortingProps {
disableCustomSorting?: boolean;
view: NavigationType;
}
export const sortOptions = [
{ name: "Last modified", id: SortOption.LastModified },
{ name: "By filename (asc)", id: SortOption.FileNameAsc },
{ name: "By filename (desc)", id: SortOption.FileNameDesc },
export const sortOptions: SortingOption[] = [
{ name: "Last modified (asc)", id: SortOption.LastModifiedAsc, order: SortOrder.asc, type: SortType.date },
{ name: "Last modified (desc)", id: SortOption.LastModifiedDesc, order: SortOrder.desc, type: SortType.date },
{ name: "By filename (asc)", id: SortOption.FileNameAsc, order: SortOrder.asc, type: SortType.string },
{ name: "By filename (desc)", id: SortOption.FileNameDesc, order: SortOrder.desc, type: SortType.string },
];
export const Sorting: React.FunctionComponent<ISortingProps> = ({}: React.PropsWithChildren<ISortingProps>) => {
export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSorting, view}: React.PropsWithChildren<ISortingProps>) => {
const [ crntSorting, setCrntSorting ] = useRecoilState(SortingAtom);
const searchValue = useRecoilValue(SearchSelector);
const settings = useRecoilValue(SettingsSelector);
const crntSort = sortOptions.find(x => x.id === crntSorting);
const updateSorting = (value: SortingOption) => {
Messenger.send(DashboardMessage.setState, {
key: `${view === NavigationType.Contents ? ExtensionState.Dashboard.Contents.Sorting : ExtensionState.Dashboard.Media.Sorting}`,
value: value
});
setCrntSorting(value)
};
let allOptions = [...sortOptions];
if (settings?.customSorting && !disableCustomSorting) {
allOptions = [...allOptions, ...settings.customSorting.map((s) => ({
title: s.title || s.name,
name: s.name,
id: s.id || `${s.name}-${s.order}`,
order: s.order,
type: s.type
}))];
}
let crntSortingOption = crntSorting;
if (!crntSortingOption) {
if (view === NavigationType.Contents) {
crntSortingOption = settings?.dashboardState?.contents?.sorting || null;
} else if (view === NavigationType.Media) {
crntSortingOption = settings?.dashboardState?.media?.sorting || null;
}
if (crntSortingOption === null) {
if (view === NavigationType.Contents && settings?.dashboardState.contents.defaultSorting) {
crntSortingOption = allOptions.find(f => f.id === settings?.dashboardState.contents.defaultSorting) || null;
} else if (view === NavigationType.Media && settings?.dashboardState.contents.defaultSorting) {
crntSortingOption = allOptions.find(f => f.id === settings?.dashboardState.contents.defaultSorting) || null;
}
}
}
let crntSort = allOptions.find(x => x.id === crntSortingOption?.id) || sortOptions[0];
return (
<div className="flex items-center">
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton label={`Sort by`} title={crntSort?.name || ""} disabled={!!searchValue} />
<MenuButton label={`Sort by`} title={crntSort?.title || crntSort?.name || ""} disabled={!!searchValue} />
<MenuItems>
{sortOptions.map((option) => (
{allOptions.map((option) => (
<MenuItem
key={option.id}
title={option.name}
value={option.id}
isCurrent={option.id === crntSorting}
onClick={(value) => setCrntSorting(value)} />
title={option.title || option.name}
value={option}
isCurrent={option.id === crntSort.id}
onClick={(value) => updateSorting(value)} />
))}
</MenuItems>
</Menu>

View File

@@ -1,9 +1,10 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { ViewAtom, ViewType, SettingsSelector } from '../../state';
import { ViewGridIcon, ViewListIcon } from '@heroicons/react/solid';
import { ViewAtom, SettingsSelector } from '../../state';
import {ViewListIcon, ViewGridIcon} from '@heroicons/react/solid';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { DashboardViewType } from '../../models';
export interface IViewSwitchProps {}
@@ -12,7 +13,7 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (props: Rea
const settings = useRecoilValue(SettingsSelector);
const toggleView = () => {
const newView = view === ViewType.Grid ? ViewType.List : ViewType.Grid;
const newView = view === DashboardViewType.Grid ? DashboardViewType.List : DashboardViewType.Grid;
setView(newView);
Messenger.send(DashboardMessage.setPageViewType, newView);
};
@@ -25,11 +26,11 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (props: Rea
return (
<div className={`flex rounded-sm bg-vulcan-50 lg:mb-1`}>
<button className={`flex items-center px-2 py-1 rounded-l-sm ${view === ViewType.Grid ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
<button className={`flex items-center px-2 py-1 rounded-l-sm ${view === DashboardViewType.Grid ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
<ViewGridIcon className={`w-4 h-4`} />
<span className={`sr-only`}>Change to grid</span>
</button>
<button className={`flex items-center px-2 py-1 rounded-r-sm ${view === ViewType.List ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
<button className={`flex items-center px-2 py-1 rounded-r-sm ${view === DashboardViewType.List ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
<ViewListIcon className={`w-4 h-4`} />
<span className={`sr-only`}>Change to list</span>
</button>

View File

@@ -1,14 +1,17 @@
import * as React from 'react';
import { FolderAddIcon } from '@heroicons/react/outline';
import {FolderAddIcon} from '@heroicons/react/outline';
import { useRecoilValue } from 'recoil';
import { DashboardMessage } from '../../DashboardMessage';
import { SelectedMediaFolderAtom } from '../../state';
import { SelectedMediaFolderAtom, SettingsSelector } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { ChoiceButton } from '../ChoiceButton';
import { CustomScript, ScriptType } from '../../../models';
export interface IFolderCreationProps {}
export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (props: React.PropsWithChildren<IFolderCreationProps>) => {
const selectedFolder = useRecoilValue(SelectedMediaFolderAtom);
const settings = useRecoilValue(SettingsSelector);
const onFolderCreation = () => {
Messenger.send(DashboardMessage.createMediaFolder, {
@@ -16,13 +19,35 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (pr
});
};
const runCustomScript = (script: CustomScript) => {
Messenger.send(DashboardMessage.runCustomScript, {script, path: selectedFolder});
};
const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder);
if (scripts.length > 0) {
return (
<div className="flex flex-1 justify-end">
<ChoiceButton
title={`Create new folder`}
choices={scripts.map(s => ({
title: s.title,
onClick: () => runCustomScript(s)
}))}
onClick={onFolderCreation}
disabled={!settings?.initialized} />
</div>
)
}
return (
<button
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
title={`Create new folder`}
onClick={onFolderCreation}>
<FolderAddIcon className={`mr-2 h-6 w-6`} />
<span className={``}>Create new folder</span>
</button>
<div className="flex flex-1 justify-end">
<button
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
title={`Create new folder`}
onClick={onFolderCreation}>
<FolderAddIcon className={`mr-2 h-6 w-6`} />
<span className={``}>Create new folder</span>
</button>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import { FolderIcon } from '@heroicons/react/solid';
import {FolderIcon} from '@heroicons/react/solid';
import { basename } from 'path';
import * as React from 'react';
import { useRecoilState } from 'recoil';
@@ -16,11 +16,13 @@ export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({ folder,
const relFolderPath = wsFolder ? folder.replace(wsFolder, '') : folder;
return (
<li className={`group relative bg-gray-200 dark:bg-vulcan-300 hover:shadow-xl dark:hover:bg-vulcan-100 text-gray-600 hover:text-gray-700 dark:text-whisper-900 dark:hover:text-whisper-800 p-4`}>
<button className={`w-full flex flex-col items-center`} onClick={() => setSelectedFolder(folder)}>
<FolderIcon className={`h-auto w-1/2`} />
<li className={`group relative hover:shadow-xl dark:hover:bg-vulcan-100 text-gray-600 hover:text-gray-700 dark:text-whisper-900 dark:hover:text-whisper-800 p-4`}>
<button className={`w-full flex flex-row items-center h-full`} onClick={() => setSelectedFolder(folder)}>
<div>
<FolderIcon className={`h-12 w-12 mr-4`} />
</div>
<p className="text-sm font-bold pointer-events-none flex items-center">
<p className="text-sm font-bold pointer-events-none flex items-center text-left overflow-hidden break-words">
{basename(relFolderPath)}
</p>
</button>

View File

@@ -1,16 +1,22 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { CheckCircleIcon, ClipboardCopyIcon, CodeIcon, PencilIcon, PhotographIcon, TrashIcon } from '@heroicons/react/outline';
import { Menu } from '@headlessui/react';
import { ClipboardIcon, CodeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { CustomScript } from '../../../helpers/CustomScript';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { ScriptType } from '../../../models';
import { MediaInfo } from '../../../models/MediaPaths';
import { DashboardMessage } from '../../DashboardMessage';
import { LightboxAtom, PageSelector, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { MenuItem, MenuItems } from '../Menu';
import { Alert } from '../Modals/Alert';
import { Metadata } from '../Modals/Metadata';
import { MenuButton } from './MenuButton'
import { QuickAction } from './QuickAction';
export interface IItemProps {
media: MediaInfo;
}
@@ -61,6 +67,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
Messenger.send(DashboardMessage.copyToClipboard, parseWinPath(relPath) || "");
};
const runCustomScript = (script: CustomScript) => {
Messenger.send(DashboardMessage.runCustomScript, {script, path: media.fsPath});
};
const insertToArticle = () => {
const relPath = getRelPath();
Messenger.send(DashboardMessage.insertPreviewImage, {
@@ -78,9 +88,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
const insertSnippet = () => {
const relPath = getRelPath();
let snippet = settings?.mediaSnippet.join("\n");
snippet = snippet?.replace("{mediaUrl}", parseWinPath(relPath) || "");
snippet = snippet?.replace("{alt}", alt || "");
snippet = snippet?.replace("{caption}", caption || "");
snippet = snippet?.replace("{filename}", basename(relPath || ""));
Messenger.send(DashboardMessage.insertPreviewImage, {
image: parseWinPath(relPath) || "",
@@ -111,8 +123,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
}
}
if (media?.stats?.size) {
const size = media.stats.size / (1024*1024);
if (media?.size) {
const size = media.size / (1024*1024);
if (size > 1) {
sizeDetails.push(`${size.toFixed(2)} MB`);
} else {
@@ -149,6 +161,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
setFilename(getFileName());
};
const customScriptActions = () => {
return (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFile).map(script => (
<MenuItem
key={script.title}
title={script.title}
onClick={() => runCustomScript(script)} />
))
}
useEffect(() => {
if (media.alt !== alt) {
setAlt(media.alt);
@@ -174,7 +195,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
return (
<>
<li className="group relative bg-gray-50 dark:bg-vulcan-200 hover:shadow-xl dark:hover:bg-vulcan-100">
<li className="group relative bg-gray-50 dark:bg-vulcan-200 hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50">
<button className="relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden cursor-pointer h-48" onClick={openLightbox}>
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
<PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />
@@ -185,54 +206,99 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
</button>
<div className={`relative py-4 pl-4 pr-12`}>
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
<button title={`Edit metadata`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={updateMetadata}>
<PencilIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Edit metadata</span>
</button>
{
viewData?.data?.filePath ? (
<>
<button
title={`Insert into your content`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={insertToArticle}>
<CheckCircleIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Insert into your content</span>
</button>
<div className="flex items-center border border-transparent group-hover:bg-gray-50 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className='hidden group-hover:inline-block h-5'>
<QuickAction
title='Edit metadata'
onClick={updateMetadata}>
<PencilIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
{
viewData?.data?.filePath ? (
<>
<QuickAction
title='Insert image with markdown markup'
onClick={insertToArticle}>
<PlusIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
{
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
<QuickAction
title='Insert snippet'
onClick={insertSnippet}>
<CodeIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
)
}
</>
) : (
<>
<QuickAction
title='Copy media path'
onClick={copyToClipboard}>
<ClipboardIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
<QuickAction
title='Delete media file'
onClick={deleteMedia}>
<TrashIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
</>
)
}
</div>
<Menu as="div" className="relative z-10 inline-block text-left h-5">
<MenuButton title={`Menu`} />
<MenuItems widthClass='w-40'>
<MenuItem
title={`Edit metadata`}
onClick={updateMetadata}
/>
{
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
<button
title={`Insert your media snippet`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={insertSnippet}>
<CodeIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Insert your media snippet</span>
</button>
viewData?.data?.filePath ? (
<>
<MenuItem
title={`Insert image markdown`}
onClick={insertToArticle} />
{
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
<MenuItem
title={`Insert snippet`}
onClick={insertSnippet} />
)
}
{ customScriptActions() }
</>
) : (
<>
<MenuItem
title={`Copy media path`}
onClick={copyToClipboard} />
{ customScriptActions() }
<MenuItem
title={`Delete`}
onClick={deleteMedia} />
</>
)
}
</>
) : (
<>
<button title={`Copy media path`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={copyToClipboard}>
<ClipboardCopyIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Copy media path</span>
</button>
<button title={`Delete media`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={deleteMedia}>
<TrashIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Delete media</span>
</button>
</>
)
}
</MenuItems>
</Menu>
</div>
</div>
<p className="text-sm dark:text-whisper-900 font-bold pointer-events-none flex items-center">
<p className="text-sm dark:text-whisper-900 font-bold pointer-events-none flex items-center break-all">
{basename(parseWinPath(media.fsPath) || "")}
</p>
{
@@ -256,7 +322,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{getFolder()}</span>
</p>
{
(media?.stats?.size || media?.dimensions) && (
(media?.size || media?.dimensions) && (
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
<b className={`mr-1`}>Size:</b>
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{calculateSize()}</span>

View File

@@ -1,10 +1,14 @@
import * as React from 'react';
export interface IListProps {}
export interface IListProps {
gap?: number;
}
export const List: React.FunctionComponent<IListProps> = ({gap, children}: React.PropsWithChildren<IListProps>) => {
const gapClass = gap !== undefined ? `gap-y-${gap}` : `gap-y-8`;
export const List: React.FunctionComponent<IListProps> = ({children}: React.PropsWithChildren<IListProps>) => {
return (
<ul role="list" className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8">
<ul role="list" className={`grid grid-cols-2 gap-x-4 ${gapClass} sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8`}>
{children}
</ul>
);

View File

@@ -1,6 +1,6 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { UploadIcon } from '@heroicons/react/outline';
import {UploadIcon} from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaInfo, MediaPaths } from '../../../models/MediaPaths';
@@ -17,19 +17,17 @@ import { useCallback } from 'react';
import { DashboardMessage } from '../../DashboardMessage';
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
import { FolderItem } from './FolderItem';
import useMedia from '../../hooks/useMedia';
export interface IMediaProps {}
export const LIMIT = 16;
export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWithChildren<IMediaProps>) => {
const { media } = useMedia();
const settings = useRecoilValue(SettingsSelector);
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const [ media, setMedia ] = React.useState<MediaInfo[]>([]);
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
const [ folders, setFolders ] = useRecoilState(MediaFoldersAtom);
const [ loading, setLoading ] = useRecoilState(LoadingAtom);
const viewData = useRecoilValue(ViewDataSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderAtom);
const folders = useRecoilValue(MediaFoldersAtom);
const loading = useRecoilValue(LoadingAtom);
const onDrop = useCallback((acceptedFiles: File[]) => {
acceptedFiles.forEach((file) => {
@@ -52,96 +50,75 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
onDrop,
accept: 'image/*'
});
const messageListener = (message: MessageEvent<EventData<MediaPaths | { key: string, value: any }>>) => {
if (message.data.command === DashboardCommand.media) {
const data: MediaPaths = message.data.data as MediaPaths;
setLoading(false);
setMedia(data.media);
setTotal(data.total);
setFolders(data.folders);
setSelectedFolder(data.selectedFolder);
}
};
React.useEffect(() => {
Messenger.listen<MediaPaths>(messageListener);
return () => {
Messenger.unlisten(messageListener);
}
}, ['']);
return (
<main className={`h-full w-full`}>
<div className="flex flex-col h-full overflow-auto">
<Header settings={settings} />
<div className="flex flex-col h-full overflow-auto">
<Header settings={settings} />
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4" {...getRootProps()}>
{
viewData?.data?.filePath && (
<div className={`text-lg text-center mb-6`}>
<p>Select the image you want to use for your article.</p>
<p className={`opacity-80 text-base`}>You can also drag and drop images from your desktop and select that once uploaded.</p>
</div>
)
}
{
isDragActive && (
<div className="absolute top-0 left-0 w-full h-full text-whisper-500 bg-gray-900 bg-opacity-70 flex flex-col justify-center items-center z-50">
<UploadIcon className={`h-32`} />
<p className={`text-xl max-w-md text-center`}>
{selectedFolder ? `Upload to ${selectedFolder}` : `No folder selected, files you drop will be added to the ${settings?.staticFolder || "public"} folder.`}
</p>
</div>
)
}
{
(media.length === 0 && folders.length === 0 && !loading) && (
<div className={`flex items-center justify-center h-full`}>
<div className={`max-w-xl text-center`}>
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
<p className={`text-xl font-medium`}>No media files to show. You can drag &amp; drop new files.</p>
</div>
</div>
)
}
{
folders && folders.length > 0 && (
<div className={`mb-8`}>
<List>
{
folders && folders.map((folder) => (
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
))
}
</List>
</div>
)
}
<List>
{
media.map((file) => (
<Item key={file.fsPath} media={file} />
))
}
</List>
</div>
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4" {...getRootProps()}>
{
loading && ( <Spinner /> )
viewData?.data?.filePath && (
<div className={`text-lg text-center mb-6`}>
<p>Select the image you want to use for your article.</p>
<p className={`opacity-80 text-base`}>You can also drag and drop images from your desktop and select that once uploaded.</p>
</div>
)
}
{
isDragActive && (
<div className="absolute top-0 left-0 w-full h-full text-whisper-500 bg-gray-900 bg-opacity-70 flex flex-col justify-center items-center z-50">
<UploadIcon className={`h-32`} />
<p className={`text-xl max-w-md text-center`}>
{selectedFolder ? `Upload to ${selectedFolder}` : `No folder selected, files you drop will be added to the ${settings?.staticFolder || "public"} folder.`}
</p>
</div>
)
}
<Lightbox />
{
(media.length === 0 && folders.length === 0 && !loading) && (
<div className={`flex items-center justify-center h-full`}>
<div className={`max-w-xl text-center`}>
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
<p className={`text-xl font-medium`}>No media files to show. You can drag &amp; drop new files.</p>
</div>
</div>
)
}
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
{
folders && folders.length > 0 && (
<div className={`mb-8`}>
<List gap={0}>
{
folders && folders.map((folder) => (
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
))
}
</List>
</div>
)
}
<List>
{
media.map((file) => (
<Item key={file.fsPath} media={file} />
))
}
</List>
</div>
</main>
{
loading && ( <Spinner /> )
}
<Lightbox />
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
</div>
);
};

View File

@@ -0,0 +1,30 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { NavigationType } from '../../models/NavigationType';
import { SettingsAtom } from '../../state';
import { Sorting } from '../Header';
import { Breadcrumb } from '../Header/Breadcrumb';
import { Pagination } from '../Header/Pagination';
export interface IMediaHeaderBottomProps {}
export const MediaHeaderBottom: React.FunctionComponent<IMediaHeaderBottomProps> = (props: React.PropsWithChildren<IMediaHeaderBottomProps>) => {
const settings = useRecoilValue(SettingsAtom);
if (!settings?.wsFolder) {
return null;
}
return (
<nav className="w-full bg-gray-200 text-vulcan-300 dark:bg-vulcan-400 dark:text-whisper-600 border-b border-gray-300 dark:border-vulcan-100 flex justify-between py-2" aria-label="Breadcrumb">
<Breadcrumb />
<Pagination />
<div className={`flex px-5 flex-1 justify-end`}>
<Sorting view={NavigationType.Media} disableCustomSorting />
</div>
</nav>
);
};

View File

@@ -0,0 +1,81 @@
import { EventData } from '@estruyf/vscode';
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebounce } from '../../../hooks/useDebounce';
import { usePrevious } from '../../../panelWebView/hooks/usePrevious';
import { DashboardCommand } from '../../DashboardCommand';
import { DashboardMessage } from '../../DashboardMessage';
import { LoadingAtom, PageAtom, SelectedMediaFolderSelector, SettingsSelector, SortingSelector } from '../../state';
import { Searchbox } from '../Header';
import { PaginationStatus } from '../Header/PaginationStatus';
import { FolderCreation } from './FolderCreation';
export interface IMediaHeaderTopProps {}
export const MediaHeaderTop: React.FunctionComponent<IMediaHeaderTopProps> = ({}: React.PropsWithChildren<IMediaHeaderTopProps>) => {
const [ lastUpdated, setLastUpdated ] = React.useState<string | null>(null);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const crntSorting = useRecoilValue(SortingSelector);
const [ , setLoading ] = useRecoilState(LoadingAtom);
const [ page, setPage ] = useRecoilState(PageAtom);
const settings = useRecoilValue(SettingsSelector);
const debounceGetMedia = useDebounce<string | null>(lastUpdated, 200);
const prevSelectedFolder = usePrevious<string | null>(selectedFolder);
const mediaUpdate = (message: MessageEvent<EventData<{ key: string, value: any }>>) => {
if (message.data.command === DashboardCommand.mediaUpdate) {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page,
folder: selectedFolder || '',
sorting: crntSorting
});
}
}
React.useEffect(() => {
if (prevSelectedFolder !== null || settings?.dashboardState?.media.selectedFolder !== selectedFolder) {
setLoading(true);
setPage(0);
setLastUpdated(new Date().getTime().toString());
}
}, [selectedFolder]);
React.useEffect(() => {
setLastUpdated(new Date().getTime().toString());
}, [crntSorting]);
React.useEffect(() => {
if (debounceGetMedia) {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page,
folder: selectedFolder || '',
sorting: crntSorting
});
}
}, [debounceGetMedia]);
React.useEffect(() => {
Messenger.listen(mediaUpdate);
return () => {
Messenger.unlisten(mediaUpdate);
}
}, []);
return (
<nav
className="py-3 px-4 flex items-center justify-between border-b border-gray-300 dark:border-vulcan-100"
aria-label="Pagination"
>
<Searchbox placeholder={`Search in folder`} />
<PaginationStatus />
<FolderCreation />
</nav>
);
};

View File

@@ -0,0 +1,20 @@
import { Menu } from '@headlessui/react';
import {DotsVerticalIcon} from '@heroicons/react/outline';
import * as React from 'react';
export interface IMenuButtonProps {
title: string;
disabled?: boolean;
}
export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({ title, disabled }: React.PropsWithChildren<IMenuButtonProps>) => {
return (
<div className={`inline-flex items-center ${disabled ? 'opacity-50' : ''}`}>
<Menu.Button disabled={disabled} className="group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600">
<span className="sr-only">{title}</span>
<DotsVerticalIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
</div>
);
};

View File

@@ -1,130 +0,0 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { RefreshIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { DashboardMessage } from '../../DashboardMessage';
import { LoadingAtom, MediaTotalSelector, PageAtom, SelectedMediaFolderSelector } from '../../state';
import { FolderCreation } from './FolderCreation';
import { LIMIT } from './Media';
import { PaginationButton } from './PaginationButton';
export interface IPaginationProps {}
export const Pagination: React.FunctionComponent<IPaginationProps> = ({}: React.PropsWithChildren<IPaginationProps>) => {
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const totalMedia = useRecoilValue(MediaTotalSelector);
const [ , setLoading ] = useRecoilState(LoadingAtom);
const [ page, setPage ] = useRecoilState(PageAtom);
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
const getTotalPage = () => {
const mediaItems = ((page + 1) * LIMIT);
if (totalMedia < mediaItems) {
return totalMedia;
}
return mediaItems;
};
// Write me function to retrieve buttons before and after current page
const getButtons = (): number[] => {
const maxButtons = 5;
const buttons: number[] = [];
const start = page - maxButtons;
const end = page + maxButtons;
for (let i = start; i <= end; i++) {
if (i >= 0 && i <= totalPages) {
buttons.push(i);
}
}
return buttons;
};
const refresh = () => {
setPage(0);
Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder });
}
React.useEffect(() => {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page,
folder: selectedFolder || ''
});
}, [page]);
React.useEffect(() => {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page: 0,
folder: selectedFolder || ''
});
setPage(0);
}, [selectedFolder]);
return (
<nav
className="py-4 px-5 flex items-center justify-between bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100"
aria-label="Pagination"
>
<div className="hidden sm:flex">
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
title="Refresh media"
onClick={refresh}>
<RefreshIcon className={`h-5 w-5`} />
<span className="sr-only">Refresh media</span>
</button>
<p className="text-sm text-gray-500 dark:text-whisper-900">
Showing <span className="font-medium">{(page * LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
<span className="font-medium">{totalMedia}</span> results
</p>
</div>
<FolderCreation />
<div className="flex justify-between sm:justify-end space-x-2 text-sm">
<PaginationButton
title="First"
disabled={page === 0}
onClick={() => {
if (page > 0) {
setPage(0)
}
}} />
<PaginationButton
title="Previous"
disabled={page === 0}
onClick={() => {
if (page > 0) {
setPage(page - 1)
}
}} />
{getButtons().map((button) => (
<button
key={button}
disabled={button === page}
onClick={() => {
setPage(button)
}
}
className={`${page === button ? 'bg-gray-200 px-2 text-vulcan-500' : 'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500'}`}
>{button + 1}</button>
))}
<PaginationButton
title="Next"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)} />
<PaginationButton
title="Last"
disabled={page >= totalPages}
onClick={() => setPage(totalPages)} />
</div>
</nav>
);
};

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
export interface IQuickActionProps {
title: string;
onClick: () => void;
}
export const QuickAction: React.FunctionComponent<IQuickActionProps> = ({title, onClick, children}: React.PropsWithChildren<IQuickActionProps>) => {
return (
<button
type='button'
title={title}
onClick={onClick}
className={`px-2 group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600`}>
{children}
<span className='sr-only'>{title}</span>
</button>
);
};

View File

@@ -1,5 +1,5 @@
import { Menu } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/solid';
import {ChevronDownIcon} from '@heroicons/react/solid';
import * as React from 'react';
export interface IMenuButtonProps {

View File

@@ -3,7 +3,7 @@ import * as React from 'react';
export interface IMenuItemProps {
title: string;
value: any;
value?: any;
isCurrent?: boolean;
disabled?: boolean;
onClick: (value: any) => void;
@@ -15,7 +15,7 @@ export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value,
<button
disabled={disabled}
onClick={() => onClick(value)}
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100 disabled:bg-gray-500`}
className={`${!isCurrent ? `font-normal` : `font-bold`} text-gray-500 dark:text-whisper-900 block px-4 py-2 text-sm w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100 disabled:bg-gray-500`}
>
{title}
</button>

View File

@@ -17,7 +17,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass,
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className={`${widthClass || "w-40"} origin-top-right absolute right-0 z-10 mt-2 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
<Menu.Items className={`${widthClass || ""} origin-top-right absolute right-0 z-10 mt-2 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
<div className="py-1">
{children}
</div>

View File

@@ -1,5 +1,5 @@
import { Dialog, Transition, } from '@headlessui/react';
import { ExclamationIcon } from '@heroicons/react/outline';
import {ExclamationIcon} from '@heroicons/react/outline';
import * as React from 'react';
import { Fragment, useRef } from 'react';

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Tab } from '../constants/Tab';
import { TabAtom } from '../state';
import { SettingsAtom, TabAtom } from '../state';
export interface INavigationProps {
totalPages: number;
@@ -15,19 +15,47 @@ export const tabs = [
export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPages}: React.PropsWithChildren<INavigationProps>) => {
const [ crntTab, setCrntTab ] = useRecoilState(TabAtom);
const settings = useRecoilValue(SettingsAtom);
return (
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<button
key={tab.name}
className={`${tab.id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
aria-current={tab.id === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(tab.id)}
>
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
</button>
))}
{
settings?.draftField?.type === "boolean" ? (
tabs.map((tab) => (
<button
key={tab.name}
className={`${tab.id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
aria-current={tab.id === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(tab.id)}
>
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
</button>
))
) : (
<>
<button
className={`${tabs[0].id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
aria-current={tabs[0].id === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(tabs[0].id)}
>
{tabs[0].name}{(tabs[0].id === crntTab && totalPages) ? ` (${totalPages})` : ''}
</button>
{
settings?.draftField?.choices?.map((value, idx) => (
<button
key={`${value}-${idx}`}
className={`${value === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm first-letter:uppercase`}
aria-current={value === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(value)}
>
{value}{(value === crntTab && totalPages) ? ` (${totalPages})` : ''}
</button>
))
}
</>
)
}
</nav>
);
};

View File

@@ -4,7 +4,7 @@ export interface ISpinnerProps {}
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
return (
<div className={`fixed top-0 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-50`}>
<div className={`fixed top-12 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-50`}>
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-50 h-32 w-32" />
</div>
);

View File

@@ -11,8 +11,8 @@ export interface ISponsorMsgProps {
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, version}: React.PropsWithChildren<ISponsorMsgProps>) => {
return (
<p className={`w-full max-w-7xl mx-auto px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center justify-between`}>
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={SPONSOR_LINK} title="Sponsor Front Matter">
<span>Sponsor</span> <HeartIcon className={`h-5 w-5 group-hover:fill-current`} />
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={SPONSOR_LINK} title="Support Front Matter">
<span>Support</span> <HeartIcon className={`h-5 w-5 group-hover:fill-current`} />
</a>
<span>Front Matter{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}</span>
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={REVIEW_LINK} title="Review Front Matter">

View File

@@ -21,7 +21,7 @@ export const Startup: React.FunctionComponent<IStartupProps> = ({settings}: Reac
}, [settings?.openOnStart]);
return (
<div className={`relative flex items-start`}>
<div className={`relative flex items-start ml-4`}>
<div className="flex items-center h-5">
<input
id="startup"

View File

@@ -1,10 +1,22 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { SettingsAtom } from '../state';
export interface IStatusProps {
draft: boolean;
draft: boolean | string;
}
export const Status: React.FunctionComponent<IStatusProps> = ({draft}: React.PropsWithChildren<IStatusProps>) => {
const settings = useRecoilValue(SettingsAtom);
if (settings?.draftField && settings.draftField.type === "choice") {
if (draft) {
return <span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 bg-teal-500`}>{draft}</span>;
} else {
return null;
}
}
return (
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 ${draft ? "bg-red-500" : "bg-teal-500"}`}>{draft ? "Draft" : "Published"}</span>
);

View File

@@ -1,25 +1,20 @@
import { CheckIcon } from '@heroicons/react/outline';
import {CheckIcon} from '@heroicons/react/outline';
import * as React from 'react';
import { Status } from '../../models/Status';
export interface IStepProps {
name: string;
description: string;
description: JSX.Element;
status: Status;
showLine: boolean;
onClick?: () => void;
onClick?: () => void | undefined;
}
export const Step: React.FunctionComponent<IStepProps> = ({name, description, status, showLine, onClick}: React.PropsWithChildren<IStepProps>) => {
return (
<>
{
showLine ? (
<div className={`-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full ${status === Status.Completed ? "bg-teal-600" : "bg-gray-300"}`} aria-hidden="true" />
) : null
}
<button className={`relative flex items-start group text-left ${onClick ? "" : "cursor-default"}`} onClick={() => { if (onClick) { onClick(); } }} disabled={!onClick}>
const renderChildren = () => {
return (
<>
{
status === Status.NotStarted && (
<span className="h-9 flex items-center" aria-hidden="true">
@@ -52,9 +47,31 @@ export const Step: React.FunctionComponent<IStepProps> = ({name, description, st
<span className="ml-4 min-w-0 flex flex-col">
<span className="text-xs font-semibold tracking-wide uppercase text-vulcan-500 dark:text-whisper-500">{name}</span>
<span className="text-sm text-vulcan-400 dark:text-whisper-600" dangerouslySetInnerHTML={{__html: description}} />
<div className="text-sm text-vulcan-400 dark:text-whisper-600">{description}</div>
</span>
</button>
</>
);
};
return (
<>
{
showLine ? (
<div className={`-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full ${status === Status.Completed ? "bg-teal-600" : "bg-gray-300"}`} aria-hidden="true" />
) : null
}
{
onClick ? (
<button className={`relative flex items-start group text-left`} onClick={() => { if (onClick) { onClick(); } }} disabled={!onClick}>
{renderChildren()}
</button>
) : (
<div className="relative flex items-start group text-left">
{renderChildren()}
</div>
)
}
</>
);
};

View File

@@ -4,36 +4,99 @@ import { DashboardMessage } from '../../DashboardMessage';
import { Settings } from '../../models/Settings';
import { Status } from '../../models/Status';
import { Step } from './Step';
import { useState } from 'react';
import { Menu } from '@headlessui/react';
import { MenuItem } from '../Menu';
import { Framework } from '../../../models';
import {ChevronDownIcon} from '@heroicons/react/outline';
import { FrameworkDetectors } from '../../../constants/FrameworkDetectors';
export interface IStepsToGetStartedProps {
settings: Settings;
}
export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps> = ({settings}: React.PropsWithChildren<IStepsToGetStartedProps>) => {
const [framework, setFramework] = useState<string | null>(null);
const frameworks: Framework[] = FrameworkDetectors.map((detector: any) => detector.framework);
const setFrameworkAndSendMessage = (framework: string) => {
setFramework(framework);
Messenger.send(DashboardMessage.setFramework, framework);
}
const steps = [
{
name: 'Initialize project',
description: 'Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.',
description: <>Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.</>,
status: settings.initialized ? Status.Completed : Status.NotStarted,
onClick: settings.initialized ? undefined : () => { Messenger.send(DashboardMessage.initializeProject); }
},
{
name: 'Framework presets',
description: (
<div>
<div>Select your site-generator or framework to prefill some of the recommended settings.</div>
<Menu as="div" className="relative inline-block text-left mt-4">
<div>
<Menu.Button className="group flex justify-center text-vulcan-500 hover:text-vulcan-600 dark:text-whisper-500 dark:hover:text-whisper-600 p-2 rounded-md border border-vulcan-400 dark:border-white">
{framework ? framework : 'Select your framework'}
<ChevronDownIcon
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-whisper-600 dark:group-hover:text-whisper-700"
aria-hidden="true"
/>
</Menu.Button>
</div>
<Menu.Items className={`w-40 origin-top-left absolute left-0 z-10 mt-2 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
<div className="py-1">
<MenuItem
title={`other`}
value={`other`}
isCurrent={!framework}
onClick={(value) => setFrameworkAndSendMessage(value)} />
<hr />
{frameworks.map((f) => (
<MenuItem
key={f.name}
title={f.name}
value={f.name}
isCurrent={f.name === framework}
onClick={(value) => setFrameworkAndSendMessage(value)} />
))}
</div>
</Menu.Items>
</Menu>
</div>
),
status: settings.crntFramework ? Status.Completed : Status.NotStarted,
onClick: undefined
},
{
name: 'Register content folders (manual action)',
description: 'Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.',
description: <>Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.</>,
status: settings.folders && settings.folders.length > 0 ? Status.Completed : Status.NotStarted
},
{
name: 'Show the dashboard',
description: 'Once both actions are completed, click on this action to load the dashboard.',
description: <>Once both actions are completed, click on this action to load the dashboard.</>,
status: (settings.initialized && settings.folders && settings.folders.length > 0) ? Status.Active : Status.NotStarted,
onClick: (settings.initialized && settings.folders && settings.folders.length > 0) ? () => { Messenger.send(DashboardMessage.reload); } : undefined
}
];
React.useEffect(() => {
if (settings.crntFramework) {
setFramework(settings.crntFramework);
}
}, [settings.crntFramework]);
return (
<nav aria-label="Progress">
<ol role="list" className="overflow-hidden">
<ol role="list">
{steps.map((step, stepIdx) => (
<li key={step.name} className={`${stepIdx !== steps.length - 1 ? 'pb-10' : ''} relative`}>
<Step name={step.name} description={step.description} status={step.status} showLine={stepIdx !== steps.length - 1} onClick={step.onClick} />

View File

@@ -1,4 +1,4 @@
import { HeartIcon, StarIcon } from '@heroicons/react/outline';
import {HeartIcon, StarIcon} from '@heroicons/react/outline';
import * as React from 'react';
import { GITHUB_LINK, REVIEW_LINK, SPONSOR_LINK } from '../../constants';
import { Messenger } from '@estruyf/vscode/dist/client';
@@ -33,11 +33,11 @@ export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({set
</h1>
<p className="mt-3 text-base text-vulcan-300 dark:text-whisper-700 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
Thank you for taking the time to test out Front Matter!
Thank you for using Front Matter!
</p>
<p className="mt-3 text-base text-vulcan-300 dark:text-whisper-700 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
We try to aim to make Front Matter as easy to use as possible, but if you have any questions, please don't hesitate to reach out to us on GitHub.
We try to aim to make Front Matter as easy to use as possible, but if you have any questions or suggestions. Please don't hesitate to reach out to us on GitHub.
</p>
<div className="mt-5 w-full sm:mx-auto sm:max-w-lg lg:ml-0">

View File

@@ -1,5 +1,6 @@
export enum SortOption {
LastModified = 1,
FileNameAsc,
FileNameDesc
LastModifiedAsc = "LastModifiedAsc",
LastModifiedDesc = "LastModifiedDesc",
FileNameAsc = "FileNameAsc",
FileNameDesc = "FileNameDesc"
}

View File

@@ -0,0 +1,75 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { useState, useEffect, useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaInfo, MediaPaths } from '../../models';
import { DashboardCommand } from '../DashboardCommand';
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, PageAtom, SearchAtom, SearchSelector, SelectedMediaFolderAtom } from '../state';
import Fuse from 'fuse.js';
const fuseOptions: Fuse.IFuseOptions<MediaInfo> = {
keys: [
{ name: 'filename', weight: 0.8 },
{ name: 'fsPath', weight: 0.5 },
{ name: 'caption', weight: 0.5 },
{ name: 'alt', weight: 0.5 }
],
threshold: 0.2,
includeScore: true
};
export const LIMIT = 16;
export default function useMedia() {
const [ media, setMedia ] = useState<MediaInfo[]>([]);
const [ page, setPage ] = useRecoilState(PageAtom);
const [ searchedMedia, setSearchedMedia ] = useState<MediaInfo[]>([]);
const [ , setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
const [ , setFolders ] = useRecoilState(MediaFoldersAtom);
const [ , setLoading ] = useRecoilState(LoadingAtom);
const search = useRecoilValue(SearchAtom);
const getMedia = useCallback(() => {
return searchedMedia.slice(page * LIMIT, ((page + 1) * LIMIT));
}, [searchedMedia, page]);
const messageListener = (message: MessageEvent<EventData<MediaPaths | { key: string, value: any }>>) => {
if (message.data.command === DashboardCommand.media) {
const data: MediaPaths = message.data.data as MediaPaths;
setLoading(false);
setMedia(data.media);
setTotal(data.total);
setFolders(data.folders);
setSelectedFolder(data.selectedFolder);
setSearchedMedia(data.media);
}
};
useEffect(() => {
if (search) {
const fuse = new Fuse(media, fuseOptions);
const results = fuse.search(search);
const newSearchedMedia = results.map(page => page.item);
setSearchedMedia(newSearchedMedia);
setTotal(results.length);
return;
}
setSearchedMedia(media);
}, [search]);
useEffect(() => {
Messenger.listen<MediaPaths>(messageListener);
return () => {
Messenger.unlisten(messageListener);
}
}, []);
return {
media: getMedia()
};
}

View File

@@ -6,6 +6,7 @@ import { Page } from '../models/Page';
import { DashboardViewAtom, SettingsAtom, ViewDataAtom } from '../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { NavigationType } from '../models';
export default function useMessages() {
const [loading, setLoading] = useState<boolean>(false);
@@ -21,8 +22,10 @@ export default function useMessages() {
break;
case DashboardCommand.viewData:
setViewData(message.data.data);
if (message.data.data?.type === 'media') {
setView('media');
if (message.data.data?.type === NavigationType.Media) {
setView(NavigationType.Media);
} else if (message.data.data?.type === NavigationType.Contents) {
setView(NavigationType.Contents);
}
break;
case DashboardCommand.settings:

View File

@@ -3,8 +3,10 @@ import { SortOption } from '../constants/SortOption';
import { Tab } from '../constants/Tab';
import { Page } from '../models/Page';
import Fuse from 'fuse.js';
import { useRecoilValue } from 'recoil';
import { CategorySelector, FolderSelector, SearchSelector, SortingSelector, TabSelector, TagSelector } from '../state';
import { useRecoilState, useRecoilValue } from 'recoil';
import { CategorySelector, FolderSelector, SearchSelector, SettingsSelector, SortingAtom, TabSelector, TagSelector } from '../state';
import { SortOrder, SortType } from '../../models';
import { Sorting } from '../../helpers/Sorting';
const fuseOptions: Fuse.IFuseOptions<Page> = {
keys: [
@@ -16,14 +18,26 @@ const fuseOptions: Fuse.IFuseOptions<Page> = {
export default function usePages(pages: Page[]) {
const [ pageItems, setPageItems ] = useState<Page[]>([]);
const [ sorting, setSorting ] = useRecoilState(SortingAtom);
const settings = useRecoilValue(SettingsSelector);
const tab = useRecoilValue(TabSelector);
const sorting = useRecoilValue(SortingSelector);
const folder = useRecoilValue(FolderSelector);
const search = useRecoilValue(SearchSelector);
const tag = useRecoilValue(TagSelector);
const category = useRecoilValue(CategorySelector);
useEffect(() => {
const draftField = settings?.draftField;
let usedSorting = sorting;
if (!usedSorting) {
const lastSort = settings?.dashboardState.contents.sorting;
if (lastSort) {
setSorting(lastSort);
return;
}
}
// Check if search needs to be performed
let searchedPages = pages;
if (search) {
@@ -34,23 +48,49 @@ export default function usePages(pages: Page[]) {
// Filter the pages
let pagesToShow: Page[] = Object.assign([], searchedPages);
if (tab === Tab.Published) {
pagesToShow = searchedPages.filter(page => !page.draft);
} else if (tab === Tab.Draft) {
pagesToShow = searchedPages.filter(page => !!page.draft);
if (draftField && draftField.type === 'choice') {
if (tab !== Tab.All) {
pagesToShow = pagesToShow.filter(page => page.fmDraft === tab);
} else {
pagesToShow = searchedPages;
}
} else {
pagesToShow = searchedPages;
const draftFieldName = draftField?.name || "draft";
if (tab === Tab.Published) {
pagesToShow = searchedPages.filter(page => !page[draftFieldName]);
} else if (tab === Tab.Draft) {
pagesToShow = searchedPages.filter(page => !!page[draftFieldName]);
} else {
pagesToShow = searchedPages;
}
}
// Sort the pages
let pagesSorted: Page[] = Object.assign([], pagesToShow);
if (!search) {
if (sorting === SortOption.FileNameAsc) {
pagesSorted = pagesToShow.sort((a, b) => a.fmFileName.toLowerCase().localeCompare(b.fmFileName.toLowerCase()));
} else if (sorting === SortOption.FileNameDesc) {
pagesSorted = pagesToShow.sort((a, b) => b.fmFileName.toLowerCase().localeCompare(a.fmFileName.toLowerCase()));
if (sorting && sorting.id === SortOption.FileNameAsc) {
pagesSorted = pagesSorted.sort(Sorting.alphabetically("fmFileName"));
} else if (sorting && sorting.id === SortOption.FileNameDesc) {
pagesSorted = pagesSorted.sort(Sorting.alphabetically("fmFileName")).reverse();
} else if (sorting && sorting.id === SortOption.LastModifiedAsc) {
pagesSorted = pagesSorted.sort(Sorting.number("fmModified"));
} else if (sorting && sorting.id === SortOption.LastModifiedDesc) {
pagesSorted = pagesSorted.sort(Sorting.number("fmModified")).reverse();
} else if (sorting && sorting.id && sorting.name) {
const { order, name, type } = sorting;
if (type === SortType.string) {
pagesSorted = pagesSorted.sort(Sorting.alphabetically(name));
} else if (type === SortType.date) {
pagesSorted = pagesSorted.sort(Sorting.date(name));
}
if (order === SortOrder.desc) {
pagesSorted = pagesSorted.reverse();
}
} else {
pagesSorted = pagesToShow.sort((a, b) => b.fmModified - a.fmModified);
pagesSorted = pagesSorted.sort(Sorting.number("fmModified")).reverse();
}
}
@@ -69,7 +109,7 @@ export default function usePages(pages: Page[]) {
}
setPageItems(pagesSorted);
}, [ pages, tab, sorting, folder, search, tag, category ]);
}, [ settings?.draftField, pages, tab, sorting, folder, search, tag, category ]);
return {
pageItems

View File

@@ -7,12 +7,6 @@ import { Integrations } from "@sentry/tracing";
import { SENTRY_LINK } from "../constants";
import './styles.css';
Sentry.init({
dsn: SENTRY_LINK,
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 0, // No performance tracing required
});
declare const acquireVsCodeApi: <T = unknown>() => {
getState: () => T;
setState: (data: T) => void;
@@ -20,5 +14,25 @@ declare const acquireVsCodeApi: <T = unknown>() => {
};
const elm = document.querySelector("#app");
const welcome = elm?.getAttribute("data-showWelcome");
render(<RecoilRoot><Dashboard showWelcome={!!welcome} /></RecoilRoot>, elm);
if (elm) {
const welcome = elm?.getAttribute("data-showWelcome");
const version = elm?.getAttribute("data-version");
const environment = elm?.getAttribute("data-environment");
const isProd = elm?.getAttribute("data-isProd");
if (isProd === "true") {
Sentry.init({
dsn: SENTRY_LINK,
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 0, // No performance tracing required
release: version || "",
environment: environment || "",
ignoreErrors: ['ResizeObserver loop limit exceeded']
});
}
render(<RecoilRoot><Dashboard showWelcome={!!welcome} /></RecoilRoot>, elm);
}
// Webpack HMR
if ((module as any).hot) (module as any).hot.accept();

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