Compare commits

...

10 Commits

Author SHA1 Message Date
Elio Struyf
0ae7cb27ce Added localization 2024-03-13 14:58:58 +01:00
Elio Struyf
5e77419f5a New table implementation 2024-03-13 14:52:06 +01:00
Elio Struyf
ec9f55b982 Localization actions 2024-03-13 11:32:42 +01:00
Elio Struyf
fdcfdc971d Multi-select hook 2024-03-12 20:40:05 +01:00
Elio Struyf
2bc103026b Editor panel + content action bar 2024-03-12 13:53:43 +01:00
Elio Struyf
23b1efec55 Multi-select actions 2024-03-11 16:52:14 +01:00
Elio Struyf
2a8d7b0ebe #671 - Implement checkbox on media card 2024-03-01 08:40:05 +01:00
Elio Struyf
3b26944a4a Update changelog 2024-02-29 08:40:01 +01:00
Elio Struyf
78cac94dd6 10.1.0 2024-02-29 08:38:46 +01:00
Elio Struyf
9c6845ed8a #768 - Update data view link 2024-02-29 08:38:31 +01:00
64 changed files with 1214 additions and 572 deletions

View File

@@ -1,5 +1,17 @@
# Change Log
## [10.1.0] - 2024-xx-xx
### ✨ New features
### 🎨 Enhancements
### ⚡️ Optimizations
### 🐞 Fixes
- [#768](https://github.com/estruyf/vscode-front-matter/issues/768): Update broken link to the documentation
## [10.0.1] - 2024-02-28
### 🐞 Fixes

View File

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

View File

@@ -37,6 +37,10 @@
"common.back": "Back",
"common.open": "Open",
"common.openWithValue": "Open: {0}",
"common.view": "View",
"common.translate": "Translate",
"common.languages": "Languages",
"common.scripts": "Scripts",
"loading.initPages": "Loading content",
@@ -146,6 +150,10 @@
"dashboard.filters.languageFilter.label": "Locale",
"dashboard.filters.languageFilter.all": "All",
"dashboard.header.actionsBar.itemsSelected": "{0} selected",
"dashboard.header.actionsBar.alertDelete.title": "Delete selected files",
"dashboard.header.actionsBar.alertDelete.description": "Are you sure you want to delete the selected files?",
"dashboard.header.breadcrumb.home": "Home",
"dashboard.header.clearFilters.title": "Clear filters, grouping, and sorting",
@@ -229,6 +237,9 @@
"dashboard.media.folderCreation.hexo.create": "Create post asset folder",
"dashboard.media.folderCreation.folder.create": "Create new folder",
"dashboard.media.folderItem.contentDirectory": "Content directory",
"dashboard.media.folderItem.publicDirectory": "Public directory",
"dashboard.media.item.buttom.insert.image": "Insert image",
"dashboard.media.item.buttom.insert.snippet": "Insert snippet",

87
package-lock.json generated
View File

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

View File

@@ -3,7 +3,7 @@
"displayName": "Front Matter CMS",
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Docusaurus, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "10.0.1",
"version": "10.1.0",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -2744,7 +2744,6 @@
},
"devDependencies": {
"@actions/core": "^1.10.0",
"@bendera/vscode-webview-elements": "0.6.2",
"@estruyf/vscode": "^1.1.0",
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1",
@@ -2769,6 +2768,7 @@
"@types/vscode": "^1.73.0",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@vscode-elements/elements": "^1.2.0",
"@vscode/l10n": "^0.0.14",
"@vscode/webview-ui-toolkit": "^1.2.2",
"@webpack-cli/serve": "^1.7.0",
@@ -2816,7 +2816,7 @@
"react-quill": "^2.0.0",
"react-router-dom": "^6.8.0",
"react-sortable-hoc": "^2.0.0",
"recoil": "^0.4.1",
"recoil": "^0.7.7",
"remark-gfm": "^3.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.8",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import { Chatbot } from './components/Chatbot/Chatbot';
import { updateCssVariables } from './utils';
import { I10nProvider } from './providers/I10nProvider';
import { SentryInit } from '../utils/sentryInit';
import { WEBSITE_LINKS } from '../constants';
declare const acquireVsCodeApi: <T = unknown>() => {
getState: () => T;
@@ -91,7 +92,7 @@ if (elm) {
render(
<I10nProvider>
<SettingsProvider
aiUrl='https://frontmatter.codes'
aiUrl={WEBSITE_LINKS.root}
experimental={experimental === 'true'}
version={version || ""}>
<Chatbot />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
import { workspace } from 'vscode';
import { Extension, Settings } from '.';
import { EXTENSION_BETA_ID, EXTENSION_ID, SETTING_TELEMETRY_DISABLE } from '../constants';
const METRICS_URL = 'https://frontmatter.codes/api/metrics';
import {
EXTENSION_BETA_ID,
EXTENSION_ID,
SETTING_TELEMETRY_DISABLE,
WEBSITE_LINKS
} from '../constants';
export class Telemetry {
private static instance: Telemetry;
@@ -77,7 +80,7 @@ export class Telemetry {
// Set a new timeout
instance.timeout = setTimeout(async () => {
await fetch(METRICS_URL, {
await fetch(WEBSITE_LINKS.api.metrics, {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@@ -151,6 +151,22 @@ export enum LocalizationKey {
* Open: {0}
*/
commonOpenWithValue = 'common.openWithValue',
/**
* View
*/
commonView = 'common.view',
/**
* Translate
*/
commonTranslate = 'common.translate',
/**
* Languages
*/
commonLanguages = 'common.languages',
/**
* Scripts
*/
commonScripts = 'common.scripts',
/**
* Loading content
*/
@@ -483,6 +499,18 @@ export enum LocalizationKey {
* All
*/
dashboardFiltersLanguageFilterAll = 'dashboard.filters.languageFilter.all',
/**
* {0} selected
*/
dashboardHeaderActionsBarItemsSelected = 'dashboard.header.actionsBar.itemsSelected',
/**
* Delete selected files
*/
dashboardHeaderActionsBarAlertDeleteTitle = 'dashboard.header.actionsBar.alertDelete.title',
/**
* Are you sure you want to delete the selected files?
*/
dashboardHeaderActionsBarAlertDeleteDescription = 'dashboard.header.actionsBar.alertDelete.description',
/**
* Home
*/
@@ -739,6 +767,14 @@ export enum LocalizationKey {
* Create new folder
*/
dashboardMediaFolderCreationFolderCreate = 'dashboard.media.folderCreation.folder.create',
/**
* Content directory
*/
dashboardMediaFolderItemContentDirectory = 'dashboard.media.folderItem.contentDirectory',
/**
* Public directory
*/
dashboardMediaFolderItemPublicDirectory = 'dashboard.media.folderItem.publicDirectory',
/**
* Insert image
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,10 +2,10 @@ import * as React from 'react';
import { useState, useMemo, useCallback, useEffect } from 'react';
import { Field, PanelSettings } from '../../../models';
import { PencilIcon } from '@heroicons/react/24/outline';
import { VsLabel } from '../VscodeComponents';
import { JsonFieldRecords, JsonFieldForm, JsonFieldSelector } from '.';
import { SortEnd } from 'react-sortable-hoc';
import { arrayMoveImmutable } from 'array-move';
import { VSCodeLabel } from '../VSCode';
export interface IJsonFieldProps {
label: string;
@@ -101,12 +101,12 @@ export const JsonField: React.FunctionComponent<IJsonFieldProps> = ({
return (
<div className="json_data__field">
<VsLabel>
<VSCodeLabel>
<div className={`metadata_field__label`}>
<PencilIcon style={{ width: '16px', height: '16px' }} />{' '}
<span style={{ lineHeight: '16px' }}>{label}</span>
</div>
</VsLabel>
</VSCodeLabel>
<JsonFieldSelector
field={field}

View File

@@ -1,10 +1,10 @@
import { CircleStackIcon, PlusIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { VsLabel } from '../VscodeComponents';
import { JsonFieldRecord } from '.';
import { SortableContainer, SortEnd } from 'react-sortable-hoc';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { VSCodeLabel } from '../VSCode';
export interface IJsonFieldRecordsProps {
records: any[];
@@ -33,7 +33,7 @@ export const JsonFieldRecords = ({
return (
<div className="json_data__list">
<VsLabel>
<VSCodeLabel>
<div className={`metadata_field__label`}>
<div>
<CircleStackIcon style={{ width: '16px', height: '16px' }} />
@@ -44,7 +44,7 @@ export const JsonFieldRecords = ({
<PlusIcon style={{ width: '16px', height: '16px' }} />
</button>
</div>
</VsLabel>
</VSCodeLabel>
<Container onSortEnd={onSort} useDragHandle>
{records.map((v: any, idx: number) => (

View File

@@ -1,14 +1,7 @@
import * as React from 'react';
import {
VsTable,
VsTableBody,
VsTableHeader,
VsTableHeaderCell,
VsTableRow,
VsTableCell
} from './VscodeComponents';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { VSCodeTable, VSCodeTableBody, VSCodeTableCell, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable';
export interface ISeoDetailsProps {
allowedLength: number;
@@ -23,30 +16,33 @@ const SeoDetails: React.FunctionComponent<ISeoDetailsProps> = (
) => {
const { allowedLength, title, value, valueTitle, noValidation } = props;
const validate = () => {
const validate = React.useMemo(() => {
if (noValidation) {
return '';
}
return value <= allowedLength ? 'valid' : 'not-valid';
};
}, [value, allowedLength, noValidation]);
return (
<div className={`seo__status__details ${validate()}`}>
<div className={`seo__status__details ${validate}`}>
<h4>{title}</h4>
<VsTable bordered>
<VsTableHeader slot="header">
<VsTableHeaderCell className={validate()}>{valueTitle}</VsTableHeaderCell>
<VsTableHeaderCell>{l10n.t(LocalizationKey.panelSeoDetailsRecommended)}</VsTableHeaderCell>
</VsTableHeader>
<VsTableBody slot="body">
<VsTableRow>
<VsTableCell className={validate()}>{value}</VsTableCell>
<VsTableCell>{allowedLength}</VsTableCell>
</VsTableRow>
</VsTableBody>
</VsTable>
<VSCodeTable>
<VSCodeTableHeader>
<VSCodeTableRow>
<VSCodeTableHead className={validate}>{valueTitle}</VSCodeTableHead>
<VSCodeTableHead>{l10n.t(LocalizationKey.panelSeoDetailsRecommended)}</VSCodeTableHead>
</VSCodeTableRow>
</VSCodeTableHeader>
<VSCodeTableBody>
<VSCodeTableRow>
<VSCodeTableCell className={validate}>{value}</VSCodeTableCell>
<VSCodeTableCell>{allowedLength}</VSCodeTableCell>
</VSCodeTableRow>
</VSCodeTableBody>
</VSCodeTable>
</div>
);
};

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { ValidInfo } from './ValidInfo';
import { VsTableCell, VsTableRow } from './VscodeComponents';
import { VSCodeTableCell, VSCodeTableRow } from './VSCode/VSCodeTable';
export interface ISeoFieldInfoProps {
title: string;
@@ -16,15 +16,11 @@ const SeoFieldInfo: React.FunctionComponent<ISeoFieldInfoProps> = ({
isValid
}: React.PropsWithChildren<ISeoFieldInfoProps>) => {
return (
<VsTableRow>
<VsTableCell className={`table__cell table__title`}>{title}</VsTableCell>
<VsTableCell className={`table__cell`}>
{value}/{recommendation}
</VsTableCell>
<VsTableCell className={`table__cell table__cell__validation`}>
{isValid !== undefined ? <ValidInfo label={undefined} isValid={isValid} /> : <span>-</span>}
</VsTableCell>
</VsTableRow>
<VSCodeTableRow>
<VSCodeTableCell className={`capitalize`}>{title}</VSCodeTableCell>
<VSCodeTableCell>{value}/{recommendation}</VSCodeTableCell>
<VSCodeTableCell>{isValid !== undefined ? <ValidInfo label={undefined} isValid={isValid} /> : <span>-</span>}</VSCodeTableCell>
</VSCodeTableRow>
);
};

View File

@@ -1,8 +1,8 @@
import * as React from 'react';
import { ValidInfo } from './ValidInfo';
import { VsTableCell, VsTableRow } from './VscodeComponents';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { VSCodeTableCell, VSCodeTableRow } from './VSCode/VSCodeTable';
export interface ISeoKeywordInfoProps {
keyword: string;
@@ -71,22 +71,22 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
}
return (
<VsTableRow>
<VsTableCell className={`table__cell`}>{keyword}</VsTableCell>
<VsTableCell className={`table__cell table__cell__validation table__cell__seo_details`}>
<div>
<VSCodeTableRow>
<VSCodeTableCell>{keyword}</VSCodeTableCell>
<VSCodeTableCell className={` table__cell__validation`}>
<div className='flex items-center'>
<ValidInfo
label={l10n.t(LocalizationKey.commonTitle)}
isValid={!!title && title.toLowerCase().includes(keyword.toLowerCase())}
/>
</div>
<div>
<div className='flex items-center'>
<ValidInfo
label={l10n.t(LocalizationKey.commonDescription)}
isValid={!!description && description.toLowerCase().includes(keyword.toLowerCase())}
/>
</div>
<div>
<div className='flex items-center'>
<ValidInfo
label={l10n.t(LocalizationKey.commonSlug)}
isValid={
@@ -96,16 +96,18 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
}
/>
</div>
<div>
<div className='flex items-center'>
<ValidInfo
label={l10n.t(LocalizationKey.panelSeoKeywordInfoValidInfoContent)}
isValid={!!content && content.toLowerCase().includes(keyword.toLowerCase())}
/>
</div>
{headings && headings.length > 0 && <div>{checkHeadings()}</div>}
{wordCount && <div>{density()}</div>}
</VsTableCell>
</VsTableRow>
{headings && headings.length > 0 &&
<div className='flex items-center'>{checkHeadings()}</div>}
{wordCount &&
<div className='flex items-center'>{density()}</div>}
</VSCodeTableCell>
</VSCodeTableRow>
);
};

View File

@@ -1,9 +1,9 @@
import * as React from 'react';
import { SeoKeywordInfo } from './SeoKeywordInfo';
import { VsTable, VsTableBody, VsTableHeader, VsTableHeaderCell } from './VscodeComponents';
import { ErrorBoundary } from '@sentry/react';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { VSCodeTable, VSCodeTableBody, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable';
export interface ISeoKeywordsProps {
keywords: string[] | null;
@@ -54,16 +54,19 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
<div className={`seo__status__keywords`}>
<h4>{l10n.t(LocalizationKey.panelSeoKeywordsTitle)}</h4>
<VsTable bordered columns={['30%', 'auto']}>
<VsTableHeader slot="header">
<VsTableHeaderCell className={`table__cell`}>
{l10n.t(LocalizationKey.panelSeoKeywordsHeaderKeyword)}
</VsTableHeaderCell>
<VsTableHeaderCell className={`table__cell`}>
{l10n.t(LocalizationKey.panelSeoKeywordsHeaderDetails)}
</VsTableHeaderCell>
</VsTableHeader>
<VsTableBody slot="body">
<VSCodeTable>
<VSCodeTableHeader>
<VSCodeTableRow>
<VSCodeTableHead>
{l10n.t(LocalizationKey.panelSeoKeywordsHeaderKeyword)}
</VSCodeTableHead>
<VSCodeTableHead>
{l10n.t(LocalizationKey.panelSeoKeywordsHeaderDetails)}
</VSCodeTableHead>
</VSCodeTableRow>
</VSCodeTableHeader>
<VSCodeTableBody>
{validateKeywords().map((keyword, index) => {
return (
<ErrorBoundary key={keyword} fallback={<div />}>
@@ -71,11 +74,11 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
</ErrorBoundary>
);
})}
</VsTableBody>
</VsTable>
</VSCodeTableBody>
</VSCodeTable>
{data.wordCount && (
<div className={`seo__status__note`}>
<div className={`text-xs mt-2`}>
{l10n.t(LocalizationKey.panelSeoKeywordsDensity)}
</div>
)}

View File

@@ -1,5 +1,4 @@
import * as React from 'react';
import { useEffect } from 'react';
import { SEO } from '../../models/PanelSettings';
import { TagType } from '../TagType';
import { ArticleDetails } from './ArticleDetails';
@@ -9,9 +8,9 @@ import { SymbolKeywordIcon } from './Icons/SymbolKeywordIcon';
import { SeoFieldInfo } from './SeoFieldInfo';
import { SeoKeywords } from './SeoKeywords';
import { TagPicker } from './TagPicker';
import { VsTable, VsTableBody, VsTableHeader, VsTableHeaderCell } from './VscodeComponents';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { VSCodeTable, VSCodeTableBody, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable';
export interface ISeoStatusProps {
seo: SEO;
@@ -27,90 +26,61 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
unsetFocus
}: React.PropsWithChildren<ISeoStatusProps>) => {
const { title, slug } = data;
const [isOpen, setIsOpen] = React.useState(true);
const tableRef = React.useRef<HTMLElement>();
const pushUpdate = React.useRef((value: boolean) => {
setTimeout(() => {
setIsOpen(value);
}, 10);
}).current;
const { descriptionField, titleField } = seo;
// Workaround for lit components not updating render
useEffect(() => {
setTimeout(() => {
let height = 0;
tableRef.current?.childNodes.forEach((elm: any) => {
height += elm.clientHeight;
});
if (height > 0 && tableRef.current) {
tableRef.current.style.height = `${height}px`;
}
}, 10);
}, [title, data[titleField], data[descriptionField], data?.articleDetails?.wordCount]);
const renderContent = () => {
if (!isOpen) {
return null;
}
const tableContent = React.useMemo(() => {
return (
<div>
<div className={`seo__status__details`}>
<h4>{l10n.t(LocalizationKey.panelSeoStatusTitle)}</h4>
<VsTable ref={tableRef} bordered zebra>
<VsTableHeader slot="header">
<VsTableHeaderCell className={`table__cell`}>
{l10n.t(LocalizationKey.panelSeoStatusHeaderProperty)}
</VsTableHeaderCell>
<VsTableHeaderCell className={`table__cell`}>
{l10n.t(LocalizationKey.panelSeoStatusHeaderLength)}
</VsTableHeaderCell>
<VsTableHeaderCell className={`table__cell`}>
{l10n.t(LocalizationKey.panelSeoStatusHeaderValid)}
</VsTableHeaderCell>
</VsTableHeader>
<VsTableBody slot="body">
{data[titleField] && seo.title > 0 && (
<VSCodeTable>
<VSCodeTableHeader>
<VSCodeTableRow>
<VSCodeTableHead>{l10n.t(LocalizationKey.panelSeoStatusHeaderProperty)}</VSCodeTableHead>
<VSCodeTableHead>{l10n.t(LocalizationKey.panelSeoStatusHeaderLength)}</VSCodeTableHead>
<VSCodeTableHead>{l10n.t(LocalizationKey.panelSeoStatusHeaderValid)}</VSCodeTableHead>
</VSCodeTableRow>
</VSCodeTableHeader>
<VSCodeTableBody>
{data[titleField] && seo.title > 0 ? (
<SeoFieldInfo
title={titleField}
value={data[titleField].length}
recommendation={l10n.t(LocalizationKey.panelSeoStatusSeoFieldInfoCharacters, seo.title)}
isValid={data[titleField].length <= seo.title}
/>
)}
) : null}
{slug && seo.slug > 0 && (
{slug && seo.slug > 0 ? (
<SeoFieldInfo
title={`slug`}
value={slug.length}
recommendation={l10n.t(LocalizationKey.panelSeoStatusSeoFieldInfoCharacters, seo.slug)}
isValid={slug.length <= seo.slug}
/>
)}
) : null}
{data[descriptionField] && seo.description > 0 && (
{data[descriptionField] && seo.description > 0 ? (
<SeoFieldInfo
title={descriptionField}
value={data[descriptionField].length}
recommendation={l10n.t(LocalizationKey.panelSeoStatusSeoFieldInfoCharacters, seo.description)}
isValid={data[descriptionField].length <= seo.description}
/>
)}
) : null}
{seo.content > 0 && data?.articleDetails?.wordCount > 0 && (
{seo.content > 0 && data?.articleDetails?.wordCount > 0 ? (
<SeoFieldInfo
title={l10n.t(LocalizationKey.panelSeoStatusSeoFieldInfoArticle)}
value={data?.articleDetails?.wordCount}
recommendation={l10n.t(LocalizationKey.panelSeoStatusSeoFieldInfoWords, seo.content)}
/>
)}
</VsTableBody>
</VsTable>
) : null}
</VSCodeTableBody>
</VSCodeTable>
</div>
<SeoKeywords
@@ -139,10 +109,10 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
<ArticleDetails details={data.articleDetails} />
</div>
);
};
}, [data, seo, focusElm, unsetFocus]);
return (
<Collapsible id={`seo`} title={l10n.t(LocalizationKey.panelSeoStatusCollapsibleTitle)} sendUpdate={pushUpdate}>
<Collapsible id={`seo`} title={l10n.t(LocalizationKey.panelSeoStatusCollapsibleTitle)}>
{!title && !data[descriptionField] ? (
<div className={`seo__status__empty`}>
<p>
@@ -150,7 +120,7 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
</p>
</div>
) : (
renderContent()
tableContent
)}
</Collapsible>
);

View File

@@ -0,0 +1,27 @@
import * as React from 'react';
export interface IVSCodeLabelProps { }
export const VSCodeLabel: React.FunctionComponent<IVSCodeLabelProps> = ({
children
}: React.PropsWithChildren<IVSCodeLabelProps>) => {
const DEFAULT_LINE_HEIGHT = 16;
const DEFAULT_FONT_SIZE = 13;
const INPUT_LINE_HEIGHT_RATIO = DEFAULT_LINE_HEIGHT / DEFAULT_FONT_SIZE;
return (
<label style={{
color: "var(--vscode-foreground)",
fontFamily: "var(--vscode-font-family)",
fontSize: "var(--vscode-font-size)",
fontWeight: "600",
lineHeight: INPUT_LINE_HEIGHT_RATIO,
cursor: "default",
display: "block",
padding: "5px 0"
}}>
{children}
</label >
);
};

View File

@@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "../../../utils/cn"
const VSCodeTable = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full text-base border-collapse indent-0 [&_tr:nth-child(2n)]:bg-[var(--vscode-keybindingTable-rowsBackground)]", className)}
{...props}
/>
</div>
))
VSCodeTable.displayName = "VSCodeTable"
const VSCodeTableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b bg-[var(--vscode-keybindingTable-headerBackground)]", className)} {...props} />
))
VSCodeTableHeader.displayName = "VSCodeTableHeader"
const VSCodeTableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
VSCodeTableBody.displayName = "VSCodeTableBody"
const VSCodeTableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
VSCodeTableFooter.displayName = "VSCodeTableFooter"
const VSCodeTableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-solid border-0 border-b border-b-[var(--vscode-editorGroup-border)] transition-colors [&_td]:border-r [&_td]:border-r-[var(--vscode-editorGroup-border)] [&_td:last-child]:border-r-0",
className
)}
{...props}
/>
))
VSCodeTableRow.displayName = "VSCodeTableRow"
const VSCodeTableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-6 px-2 py-2 text-left align-middle font-bold",
className
)}
{...props}
/>
))
VSCodeTableHead.displayName = "VSCodeTableHead"
const VSCodeTableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("border-solid border-0 h-6 px-2 overflow-hidden align-middle", className)}
{...props}
/>
))
VSCodeTableCell.displayName = "VSCodeTableCell"
export {
VSCodeTable,
VSCodeTableHeader,
VSCodeTableBody,
VSCodeTableFooter,
VSCodeTableHead,
VSCodeTableRow,
VSCodeTableCell,
}

View File

@@ -0,0 +1 @@
export * from './VSCodeLabel';

View File

@@ -1,6 +1,5 @@
import * as React from 'react';
import { CheckIcon } from './Icons/CheckIcon';
import { WarningIcon } from './Icons/WarningIcon';
import { CheckIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline';
export interface IValidInfoProps {
label?: string;
@@ -14,13 +13,9 @@ const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({
return (
<>
{isValid ? (
<span className="valid">
<CheckIcon />
</span>
<CheckIcon className={`h-4 w-4 text-[#46ec86] mr-2`} />
) : (
<span className="warning">
<WarningIcon />
</span>
<ExclamationTriangleIcon className={`h-4 w-4 text-[var(--vscode-statusBarItem-warningBackground)] mr-2`} />
)}
{label && <span>{label}</span>}
</>

View File

@@ -1,11 +1,5 @@
import { wrapWc } from 'wc-react';
// @bendera/vscode-webview-elements
export const VsTable = wrapWc(`vscode-table`);
export const VsTableHeader = wrapWc(`vscode-table-header`);
export const VsTableHeaderCell = wrapWc(`vscode-table-header-cell`);
export const VsTableBody = wrapWc(`vscode-table-body`);
export const VsTableRow = wrapWc(`vscode-table-row`);
export const VsTableCell = wrapWc(`vscode-table-cell`);
export const VsCollapsible = wrapWc(`vscode-collapsible`);
export const VsLabel = wrapWc(`vscode-label`);
// export const VsLabel = wrapWc(`vscode-label`);

View File

@@ -9,14 +9,8 @@ import { SentryInit } from '../utils/sentryInit';
import './styles.css';
// require('@vscode/codicons/dist/codicon.css');
import '@bendera/vscode-webview-elements/dist/vscode-table.js';
import '@bendera/vscode-webview-elements/dist/vscode-table-header.js';
import '@bendera/vscode-webview-elements/dist/vscode-table-header-cell.js';
import '@bendera/vscode-webview-elements/dist/vscode-table-body.js';
import '@bendera/vscode-webview-elements/dist/vscode-table-row.js';
import '@bendera/vscode-webview-elements/dist/vscode-table-cell.js';
import '@bendera/vscode-webview-elements/dist/vscode-collapsible.js';
import '@bendera/vscode-webview-elements/dist/vscode-label.js';
import '@vscode-elements/elements/dist/vscode-collapsible/index.js';
// import '@bendera/vscode-webview-elements/dist/vscode-label/index.js';
// import '@bendera/vscode-webview-elements/dist/vscode-checkbox.js';
// import '@vscode/webview-ui-toolkit/dist/esm/checkbox';

View File

@@ -1,11 +1,15 @@
import { SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from '../constants';
import {
SETTING_SEO_DESCRIPTION_LENGTH,
SETTING_SEO_TITLE_LENGTH,
WEBSITE_LINKS
} from '../constants';
import { Logger, Notifications, Settings, TaxonomyHelper } from '../helpers';
import { TagType } from '../panelWebView/TagType';
import { TaxonomyType } from '../models';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../localization';
const AI_URL = 'https://frontmatter.codes/api/ai';
const AI_URL = WEBSITE_LINKS.api.ai;
// const AI_URL = 'http://localhost:3000/api/ai';
export class SponsorAi {