mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-07-06 09:51:29 +02:00
Grouping and status tabs enhancements
This commit is contained in:
+1
-2
@@ -8,13 +8,12 @@
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- Grouping and status tabs enhancements
|
||||
- [#570](https://github.com/estruyf/vscode-front-matter/issues/570): Clear empty values on content creation and editing
|
||||
- [#645](https://github.com/estruyf/vscode-front-matter/issues/645): French localization added (thanks to [Clément Barbaza](https://github.com/cba85))
|
||||
- [#649](https://github.com/estruyf/vscode-front-matter/issues/649): Parse optional variables from snippets
|
||||
- [#652](https://github.com/estruyf/vscode-front-matter/issues/652): Show the start/stop server buttons depending on the local terminal session
|
||||
|
||||
### ⚡️ Optimizations
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#646](https://github.com/estruyf/vscode-front-matter/issues/646): Update the Astro `3000` port to `4321`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { format } from 'date-fns';
|
||||
import { format as fnsFormat } from 'date-fns';
|
||||
import * as React from 'react';
|
||||
import { DateHelper } from '../../../helpers/DateHelper';
|
||||
import useThemeColors from '../../hooks/useThemeColors';
|
||||
@@ -6,24 +6,32 @@ import useThemeColors from '../../hooks/useThemeColors';
|
||||
export interface IDateFieldProps {
|
||||
className?: string;
|
||||
value: Date | string;
|
||||
format?: string;
|
||||
}
|
||||
|
||||
export const DateField: React.FunctionComponent<IDateFieldProps> = ({
|
||||
className,
|
||||
value
|
||||
value,
|
||||
format
|
||||
}: React.PropsWithChildren<IDateFieldProps>) => {
|
||||
const [dateValue, setDateValue] = React.useState<string>('');
|
||||
const { getColors } = useThemeColors();
|
||||
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
const parsedValue = typeof value === 'string' ? DateHelper.tryParse(value) : value;
|
||||
const dateString = parsedValue ? format(parsedValue, 'yyyy-MM-dd') : parsedValue;
|
||||
setDateValue(dateString || '');
|
||||
const parsedValue = typeof value === 'string' ? DateHelper.tryParse(value, format) : value;
|
||||
const dateString = parsedValue ? fnsFormat(parsedValue, 'yyyy-MM-dd') : parsedValue;
|
||||
|
||||
if (dateString) {
|
||||
setDateValue(dateString);
|
||||
} else if (!dateString && typeof value === 'string') {
|
||||
setDateValue(value);
|
||||
}
|
||||
} catch (e) {
|
||||
// Date is invalid
|
||||
setDateValue(typeof value === 'string' ? value : '');
|
||||
}
|
||||
}, [value]);
|
||||
}, [value, format]);
|
||||
|
||||
if (!dateValue) {
|
||||
return null;
|
||||
|
||||
@@ -21,6 +21,7 @@ const PREVIEW_IMAGE_FIELD = 'fmPreviewImage';
|
||||
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
fmFilePath,
|
||||
fmDateFormat,
|
||||
date,
|
||||
title,
|
||||
description,
|
||||
@@ -174,7 +175,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
dateHtml ? (
|
||||
<div className='mr-4' dangerouslySetInnerHTML={{ __html: dateHtml }} />
|
||||
) : (
|
||||
cardFields?.date && <DateField className={`mr-4`} value={date} />
|
||||
cardFields?.date && <DateField className={`mr-4`} value={date} format={fmDateFormat} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Menu } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { GroupOption } from '../../constants/GroupOption';
|
||||
import { GroupingAtom } from '../../state';
|
||||
import { AllPagesAtom, GroupingAtom, PageAtom } from '../../state';
|
||||
import { MenuButton, MenuItem, MenuItems } from '../Menu';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
@@ -13,15 +13,34 @@ export const Grouping: React.FunctionComponent<
|
||||
IGroupingProps
|
||||
> = ({ }: React.PropsWithChildren<IGroupingProps>) => {
|
||||
const [group, setGroup] = useRecoilState(GroupingAtom);
|
||||
const pages = useRecoilValue(AllPagesAtom);
|
||||
|
||||
const GROUP_OPTIONS = [
|
||||
{ name: l10n.t(LocalizationKey.dashboardHeaderGroupingOptionNone), id: GroupOption.none },
|
||||
{ name: l10n.t(LocalizationKey.dashboardHeaderGroupingOptionYear), id: GroupOption.Year },
|
||||
{ name: l10n.t(LocalizationKey.dashboardHeaderGroupingOptionDraft), id: GroupOption.Draft }
|
||||
];
|
||||
const GROUP_OPTIONS = React.useMemo(() => {
|
||||
let options: { name: string, id: GroupOption }[] = [];
|
||||
|
||||
if (pages.length > 0) {
|
||||
if (pages.some((x) => x.fmYear)) {
|
||||
options.push({ name: l10n.t(LocalizationKey.dashboardHeaderGroupingOptionYear), id: GroupOption.Year })
|
||||
}
|
||||
|
||||
if (pages.some((x) => x.fmDraft)) {
|
||||
options.push({ name: l10n.t(LocalizationKey.dashboardHeaderGroupingOptionDraft), id: GroupOption.Draft })
|
||||
}
|
||||
}
|
||||
|
||||
if (options.length > 0) {
|
||||
options.unshift({ name: l10n.t(LocalizationKey.dashboardHeaderGroupingOptionNone), id: GroupOption.none })
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [pages])
|
||||
|
||||
const crntGroup = GROUP_OPTIONS.find((x) => x.id === group);
|
||||
|
||||
if (GROUP_OPTIONS.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Tab } from '../../constants/Tab';
|
||||
import useThemeColors from '../../hooks/useThemeColors';
|
||||
import { SettingsAtom, TabAtom, TabInfoAtom } from '../../state';
|
||||
import { AllPagesAtom, SettingsAtom, TabAtom, TabInfoAtom } from '../../state';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
|
||||
@@ -22,7 +21,6 @@ const NavigationItem: React.FunctionComponent<INavigationItemProps> = ({
|
||||
onClick,
|
||||
children
|
||||
}: React.PropsWithChildren<INavigationItemProps>) => {
|
||||
const { getColors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -40,8 +38,9 @@ const NavigationItem: React.FunctionComponent<INavigationItemProps> = ({
|
||||
};
|
||||
|
||||
export const Navigation: React.FunctionComponent<INavigationProps> = ({
|
||||
totalPages
|
||||
|
||||
}: React.PropsWithChildren<INavigationProps>) => {
|
||||
const pages = useRecoilValue(AllPagesAtom);
|
||||
const [crntTab, setCrntTab] = useRecoilState(TabAtom);
|
||||
const tabInfo = useRecoilValue(TabInfoAtom);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
@@ -52,6 +51,14 @@ export const Navigation: React.FunctionComponent<INavigationProps> = ({
|
||||
{ name: l10n.t(LocalizationKey.dashboardHeaderNavigationDraft), id: Tab.Draft }
|
||||
];
|
||||
|
||||
const usesDraft = React.useMemo(() => {
|
||||
return pages.some((x) => x.fmDraft);
|
||||
}, [pages]);
|
||||
|
||||
if (!usesDraft) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
|
||||
{settings?.draftField?.type === 'boolean' ? (
|
||||
|
||||
@@ -23,7 +23,7 @@ import { LocalizationKey } from '../../../localization';
|
||||
export interface IRefreshDashboardDataProps { }
|
||||
|
||||
export const RefreshDashboardData: React.FunctionComponent<IRefreshDashboardDataProps> = (
|
||||
props: React.PropsWithChildren<IRefreshDashboardDataProps>
|
||||
{ }: React.PropsWithChildren<IRefreshDashboardDataProps>
|
||||
) => {
|
||||
const view = useRecoilValue(DashboardViewAtom);
|
||||
const [, setLoading] = useRecoilState(LoadingAtom);
|
||||
@@ -35,7 +35,6 @@ export const RefreshDashboardData: React.FunctionComponent<IRefreshDashboardData
|
||||
// Media
|
||||
const resetPage = useResetRecoilState(PageAtom);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const { getColors } = useThemeColors();
|
||||
|
||||
const refreshPages = () => {
|
||||
setLoading(true);
|
||||
@@ -64,11 +63,7 @@ export const RefreshDashboardData: React.FunctionComponent<IRefreshDashboardData
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`mr-2 ${getColors(
|
||||
'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500',
|
||||
'text-[var(--vscode-foreground)] hover:text-[var(--vscode-textLink-foreground)]'
|
||||
)
|
||||
}`}
|
||||
className={`mr-2 text-[var(--vscode-foreground)] hover:text-[var(--vscode-textLink-foreground)]`}
|
||||
title={l10n.t(LocalizationKey.dashboardHeaderRefreshDashboardLabel)}
|
||||
onClick={refresh}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { CheckCircleIcon } from '@heroicons/react/outline';
|
||||
import { CheckCircleIcon as CheckCircleIconSolid } from '@heroicons/react/solid';
|
||||
import { CheckCircleIcon, PlusCircleIcon } from '@heroicons/react/outline';
|
||||
import { CheckCircleIcon as CheckCircleIconSolid, PlusCircleIcon as PlusCircleIconSolid } from '@heroicons/react/solid';
|
||||
|
||||
export interface ISelectItemProps {
|
||||
title: string;
|
||||
icon?: "add" | "select";
|
||||
buttonTitle: string;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
@@ -11,6 +12,7 @@ export interface ISelectItemProps {
|
||||
|
||||
export const SelectItem: React.FunctionComponent<ISelectItemProps> = ({
|
||||
title,
|
||||
icon = "select",
|
||||
buttonTitle,
|
||||
isSelected,
|
||||
onClick
|
||||
@@ -25,9 +27,17 @@ export const SelectItem: React.FunctionComponent<ISelectItemProps> = ({
|
||||
title={buttonTitle}
|
||||
>
|
||||
{isSelected ? (
|
||||
<CheckCircleIconSolid className={`h-4 w-4`} />
|
||||
icon === "add" ? (
|
||||
<PlusCircleIconSolid className={`h-4 w-4`} />
|
||||
) : (
|
||||
<CheckCircleIconSolid className={`h-4 w-4`} />
|
||||
)
|
||||
) : (
|
||||
<CheckCircleIcon className={`h-4 w-4`} />
|
||||
icon === "add" ? (
|
||||
<PlusCircleIcon className={`h-4 w-4`} />
|
||||
) : (
|
||||
<CheckCircleIcon className={`h-4 w-4`} />
|
||||
)
|
||||
)}
|
||||
<span>{title}</span>
|
||||
</button>
|
||||
|
||||
@@ -189,6 +189,7 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
<SelectItem
|
||||
key={template.id}
|
||||
title={template.name}
|
||||
icon='add'
|
||||
buttonTitle={template.name}
|
||||
isSelected={false}
|
||||
onClick={() => triggerTemplate(template)} />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Tab } from '../constants/Tab';
|
||||
import { Page } from '../models/Page';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import {
|
||||
AllPagesAtom,
|
||||
CategorySelector,
|
||||
FolderSelector,
|
||||
SearchSelector,
|
||||
@@ -19,9 +20,10 @@ import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../DashboardMessage';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { parseWinPath } from '../../helpers/parseWinPath';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export default function usePages(pages: Page[]) {
|
||||
const [pageItems, setPageItems] = useState<Page[]>([]);
|
||||
const [pageItems, setPageItems] = useRecoilState(AllPagesAtom);
|
||||
const [sortedPages, setSortedPages] = useState<Page[]>([]);
|
||||
const [sorting, setSorting] = useRecoilState(SortingAtom);
|
||||
const [tabInfo, setTabInfo] = useRecoilState(TabInfoAtom);
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface Page {
|
||||
fmTags: string[];
|
||||
fmCategories: string[];
|
||||
fmContentType: string;
|
||||
fmDateFormat: string | undefined;
|
||||
|
||||
title: string;
|
||||
slug: string;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { atom } from 'recoil';
|
||||
import { Page } from '../../models';
|
||||
|
||||
export const AllPagesAtom = atom<Page[]>({
|
||||
key: 'AllPagesAtom',
|
||||
default: []
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './AllContentFoldersAtom';
|
||||
export * from './AllPagesAtom';
|
||||
export * from './AllStaticFoldersAtom';
|
||||
export * from './CategoryAtom';
|
||||
export * from './DashboardViewAtom';
|
||||
|
||||
@@ -349,6 +349,25 @@ export class ContentType {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the field by its name
|
||||
* @param fields
|
||||
* @param name
|
||||
* @returns
|
||||
*/
|
||||
public static findFieldByName(fields: Field[], name: string): Field | undefined {
|
||||
for (const field of fields) {
|
||||
if (field.name === name) {
|
||||
return field;
|
||||
} else if (field.type === 'fields' && field.fields) {
|
||||
const subField = this.findFieldByName(field.fields, name);
|
||||
if (subField) {
|
||||
return subField;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the field by its type
|
||||
* @param fields
|
||||
|
||||
@@ -77,15 +77,15 @@ export const ContentTypeValidator: React.FunctionComponent<IContentTypeValidator
|
||||
|
||||
|
||||
<div className="hint__buttons">
|
||||
<VSCodeButton appearance={`secondary`} onClick={generateContentType}>
|
||||
<VSCodeButton style={{ "--border-width": 0 }} appearance={`secondary`} onClick={generateContentType}>
|
||||
{l10n.t(LocalizationKey.panelContentTypeContentTypeValidatorButtonCreate)}
|
||||
</VSCodeButton>
|
||||
|
||||
<VSCodeButton appearance={`secondary`} onClick={addMissingFields}>
|
||||
<VSCodeButton style={{ "--border-width": 0 }} appearance={`secondary`} onClick={addMissingFields}>
|
||||
{l10n.t(LocalizationKey.panelContentTypeContentTypeValidatorButtonAdd)}
|
||||
</VSCodeButton>
|
||||
|
||||
<VSCodeButton appearance={`secondary`} onClick={setContentType}>
|
||||
<VSCodeButton style={{ "--border-width": 0 }} appearance={`secondary`} onClick={setContentType}>
|
||||
{l10n.t(LocalizationKey.panelContentTypeContentTypeValidatorButtonChange)}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
+16
-10
@@ -1,7 +1,7 @@
|
||||
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
|
||||
import { parseWinPath } from './../helpers/parseWinPath';
|
||||
import { dirname, extname, join } from 'path';
|
||||
import { StatusBarAlignment, Uri, window, Webview } from 'vscode';
|
||||
import { StatusBarAlignment, Uri, window } from 'vscode';
|
||||
import { Dashboard } from '../commands/Dashboard';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import {
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
DEFAULT_CONTENT_TYPE_NAME,
|
||||
ExtensionState,
|
||||
SETTING_SEO_DESCRIPTION_FIELD,
|
||||
SETTING_SEO_TITLE_FIELD
|
||||
SETTING_SEO_TITLE_FIELD,
|
||||
SETTING_DATE_FORMAT
|
||||
} from '../constants';
|
||||
import { Page } from '../dashboardWebView/models';
|
||||
import {
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
Settings
|
||||
} from '../helpers';
|
||||
import { existsAsync } from '../utils';
|
||||
import { Article } from '../commands';
|
||||
import { Article, Cache } from '../commands';
|
||||
|
||||
export class PagesParser {
|
||||
public static allPages: Page[] = [];
|
||||
@@ -65,6 +66,7 @@ export class PagesParser {
|
||||
public static async reset() {
|
||||
this.parser = undefined;
|
||||
PagesParser.allPages = [];
|
||||
await Cache.clear(false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,8 +169,16 @@ export class PagesParser {
|
||||
(Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string) || DefaultFields.Description;
|
||||
|
||||
const dateField = ArticleHelper.getPublishDateField(article) || DefaultFields.PublishingDate;
|
||||
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
let dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
const ctDateField = ContentType.findFieldByName(contentType.fields, dateField);
|
||||
if (ctDateField && ctDateField.dateFormat) {
|
||||
dateFormat = ctDateField.dateFormat;
|
||||
}
|
||||
|
||||
const dateFieldValue = article?.data[dateField]
|
||||
? DateHelper.tryParse(article?.data[dateField])
|
||||
? DateHelper.tryParse(article?.data[dateField], dateFormat)
|
||||
: undefined;
|
||||
|
||||
const modifiedField = ArticleHelper.getModifiedDateField(article) || null;
|
||||
@@ -206,8 +216,9 @@ export class PagesParser {
|
||||
fmPreviewImage: '',
|
||||
fmTags: [],
|
||||
fmCategories: [],
|
||||
fmContentType: DEFAULT_CONTENT_TYPE_NAME,
|
||||
fmContentType: contentType.name || DEFAULT_CONTENT_TYPE_NAME,
|
||||
fmBody: article?.content || '',
|
||||
fmDateFormat: dateFormat,
|
||||
// Make sure these are always set
|
||||
title: escapedTitle,
|
||||
description: escapedDescription,
|
||||
@@ -216,11 +227,6 @@ export class PagesParser {
|
||||
draft: article?.data.draft
|
||||
};
|
||||
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
if (contentType) {
|
||||
page.fmContentType = contentType.name;
|
||||
}
|
||||
|
||||
let previewFieldParents = ContentType.findPreviewField(contentType.fields);
|
||||
if (previewFieldParents.length === 0) {
|
||||
const previewField = contentType.fields.find(
|
||||
|
||||
Reference in New Issue
Block a user