Grouping and status tabs enhancements

This commit is contained in:
Elio Struyf
2023-09-07 14:50:13 +02:00
parent c5cb1bbb06
commit 3f578fdfa9
15 changed files with 121 additions and 45 deletions
+1 -2
View File
@@ -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)} />
+3 -1
View File
@@ -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);
+1
View File
@@ -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
View File
@@ -1,4 +1,5 @@
export * from './AllContentFoldersAtom';
export * from './AllPagesAtom';
export * from './AllStaticFoldersAtom';
export * from './CategoryAtom';
export * from './DashboardViewAtom';
+19
View File
@@ -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
View File
@@ -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(