mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
330 lines
12 KiB
TypeScript
330 lines
12 KiB
TypeScript
import * as React from 'react';
|
|
import { Header } from '../Header';
|
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
|
import { DashboardViewAtom, SettingsSelector } from '../../state';
|
|
import { DataForm } from './DataForm';
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { DataFile } from '../../../models/DataFile';
|
|
import { Messenger } from '@estruyf/vscode/dist/client';
|
|
import { DashboardMessage } from '../../DashboardMessage';
|
|
import { SponsorMsg } from '../Layout/SponsorMsg';
|
|
import { EventData } from '@estruyf/vscode';
|
|
import { DashboardCommand } from '../../DashboardCommand';
|
|
import { Button } from '../Common/Button';
|
|
import { arrayMoveImmutable } from 'array-move';
|
|
import { EmptyView } from './EmptyView';
|
|
import { Container } from './SortableContainer';
|
|
import { SortableItem } from './SortableItem';
|
|
import { ChevronRightIcon, DatabaseIcon } from '@heroicons/react/outline';
|
|
import { ToastContainer, toast, Slide } from 'react-toastify';
|
|
import 'react-toastify/dist/ReactToastify.css';
|
|
import { DataType } from '../../../models/DataType';
|
|
import { TelemetryEvent } from '../../../constants';
|
|
import { NavigationItem } from '../Layout';
|
|
import useThemeColors from '../../hooks/useThemeColors';
|
|
import * as l10n from '@vscode/l10n';
|
|
import { LocalizationKey } from '../../../localization';
|
|
import { NavigationType } from '../../models';
|
|
|
|
export interface IDataViewProps { }
|
|
|
|
export const DataView: React.FunctionComponent<IDataViewProps> = (
|
|
props: React.PropsWithChildren<IDataViewProps>
|
|
) => {
|
|
const [selectedData, setSelectedData] = useState<DataFile | null>(null);
|
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
|
const [dataEntries, setDataEntries] = useState<any | any[] | null>(null);
|
|
const settings = useRecoilValue(SettingsSelector);
|
|
const { getColors } = useThemeColors();
|
|
const [, setView] = useRecoilState(DashboardViewAtom);
|
|
|
|
const setSchema = (dataFile: DataFile) => {
|
|
setSelectedData(dataFile);
|
|
setSelectedIndex(null);
|
|
setDataEntries(null);
|
|
|
|
Messenger.send(DashboardMessage.getDataEntries, { ...dataFile });
|
|
};
|
|
|
|
const messageListener = (message: MessageEvent<EventData<any>>) => {
|
|
if (message.data.command === DashboardCommand.dataFileEntries) {
|
|
setDataEntries(message.data.payload);
|
|
}
|
|
};
|
|
|
|
const deleteItem = useCallback(
|
|
(index: number) => {
|
|
const dataClone: any[] = Object.assign([], dataEntries);
|
|
|
|
if (!selectedData) {
|
|
return;
|
|
}
|
|
|
|
dataClone.splice(index, 1);
|
|
updateData(dataClone);
|
|
},
|
|
[selectedData, dataEntries]
|
|
);
|
|
|
|
const onSubmit = useCallback(
|
|
(data: any) => {
|
|
if (selectedData?.singleEntry) {
|
|
// Needs to add a single entry
|
|
updateData(data);
|
|
return;
|
|
}
|
|
|
|
const dataClone: any[] = Object.assign([], dataEntries);
|
|
if (selectedIndex !== null && selectedIndex !== undefined) {
|
|
dataClone[selectedIndex] = data;
|
|
} else {
|
|
dataClone.push(data);
|
|
}
|
|
updateData(dataClone);
|
|
},
|
|
[selectedData, dataEntries, selectedIndex]
|
|
);
|
|
|
|
const onSortEnd = useCallback(
|
|
({ oldIndex, newIndex }: any) => {
|
|
if (!dataEntries || dataEntries.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
if (selectedIndex !== null && selectedIndex !== undefined) {
|
|
setSelectedIndex(newIndex);
|
|
}
|
|
|
|
const newEntries = arrayMoveImmutable(dataEntries, oldIndex, newIndex);
|
|
updateData(newEntries);
|
|
},
|
|
[selectedData, dataEntries, selectedIndex]
|
|
);
|
|
|
|
const updateData = useCallback(
|
|
(data: any) => {
|
|
if (!selectedData) {
|
|
return;
|
|
}
|
|
|
|
Messenger.send(DashboardMessage.putDataEntries, {
|
|
file: selectedData.file,
|
|
fileType: selectedData.fileType,
|
|
entries: data
|
|
});
|
|
|
|
// Show toast message
|
|
toast.success('Updated your data entries', {
|
|
position: 'top-right',
|
|
autoClose: 2000,
|
|
hideProgressBar: true,
|
|
closeOnClick: true,
|
|
pauseOnHover: false,
|
|
transition: Slide
|
|
});
|
|
},
|
|
[selectedData]
|
|
);
|
|
|
|
const dataEntry = useMemo(() => {
|
|
if (selectedData?.singleEntry) {
|
|
return dataEntries || {};
|
|
}
|
|
|
|
return dataEntries && selectedIndex !== null && selectedIndex !== undefined
|
|
? dataEntries[selectedIndex]
|
|
: null;
|
|
}, [selectedData, , dataEntries, selectedIndex]);
|
|
|
|
useEffect(() => {
|
|
setView(NavigationType.Data);
|
|
Messenger.listen(messageListener);
|
|
|
|
Messenger.send(DashboardMessage.sendTelemetry, {
|
|
event: TelemetryEvent.webviewDataView
|
|
});
|
|
|
|
return () => {
|
|
Messenger.unlisten(messageListener);
|
|
};
|
|
}, []);
|
|
|
|
// Retrieve the data files, check if they have a schema or ID, if not, they shouldn't be shown
|
|
const dataFiles = (settings?.dataFiles || [])
|
|
.map((dataFile: DataFile) => {
|
|
if (!dataFile.schema && !dataFile.id) {
|
|
return null;
|
|
}
|
|
|
|
const clonedFile = Object.assign({}, dataFile);
|
|
|
|
if (clonedFile.type) {
|
|
const dataType = settings?.dataTypes?.find(
|
|
(dataType: DataType) => dataType.id === clonedFile.type
|
|
);
|
|
if (!dataType) {
|
|
return null;
|
|
}
|
|
clonedFile.schema = Object.assign({}, dataType.schema);
|
|
}
|
|
|
|
return clonedFile;
|
|
})
|
|
.filter((d) => d !== null) as DataFile[];
|
|
|
|
return (
|
|
<div className="flex flex-col h-full overflow-auto inset-y-0">
|
|
<Header settings={settings} />
|
|
|
|
{settings?.dataFiles && settings.dataFiles.length > 0 ? (
|
|
<div className="relative w-full flex-grow mx-auto overflow-hidden">
|
|
<div className={`flex w-64 flex-col absolute inset-y-0`}>
|
|
<aside
|
|
className={`flex flex-col flex-grow overflow-y-auto border-r py-6 px-4 overflow-auto ${getColors(
|
|
'border-gray-200 dark:border-vulcan-300',
|
|
'border-[var(--frontmatter-border)]'
|
|
)
|
|
}`}
|
|
>
|
|
<h2 className={`text-lg ${getColors(
|
|
`text-gray-500 dark:text-whisper-900`,
|
|
`text-[var(--frontmatter-text)]`
|
|
)
|
|
}`}>
|
|
{l10n.t(LocalizationKey.dashboardDataViewDataViewSelect)}
|
|
</h2>
|
|
|
|
<nav className={`flex-1 py-4 -mx-4`}>
|
|
<div
|
|
className={`divide-y border-t border-b ${getColors(
|
|
`divide-gray-200 dark:divide-vulcan-300 border-gray-200 dark:border-vulcan-300`,
|
|
`divide-[var(--frontmatter-border)] border-[var(--frontmatter-border)]`
|
|
)
|
|
}`}
|
|
>
|
|
{dataFiles &&
|
|
dataFiles.length > 0 &&
|
|
dataFiles.map((dataFile, idx) => (
|
|
<NavigationItem
|
|
key={`${dataFile.id}-${idx}`}
|
|
isSelected={selectedData?.id === dataFile.id}
|
|
onClick={() => setSchema(dataFile)}
|
|
>
|
|
<ChevronRightIcon className="-ml-1 w-5 mr-2" />
|
|
<span>{dataFile.title}</span>
|
|
</NavigationItem>
|
|
))}
|
|
</div>
|
|
</nav>
|
|
</aside>
|
|
</div>
|
|
|
|
<section className={`pl-64 flex min-w-0 h-full`}>
|
|
{selectedData ? (
|
|
<>
|
|
{!selectedData.singleEntry && (
|
|
<div
|
|
className={`w-1/3 py-6 px-4 flex-1 border-r overflow-auto ${getColors(
|
|
`border-gray-200 dark:border-vulcan-300`,
|
|
`border-[var(--frontmatter-border)]`
|
|
)
|
|
}`}
|
|
>
|
|
<h2 className={`text-lg ${getColors(
|
|
`text-gray-500 dark:text-whisper-900`,
|
|
`text-[var(--frontmatter-text)]`
|
|
)
|
|
}`}>
|
|
{l10n.t(LocalizationKey.dashboardDataViewDataViewTitle, selectedData?.title?.toLowerCase() || '')}
|
|
</h2>
|
|
|
|
<div className="py-4">
|
|
{dataEntries && dataEntries.length > 0 ? (
|
|
<>
|
|
<Container onSortEnd={onSortEnd} useDragHandle>
|
|
{((dataEntries as any[]) || []).map((dataEntry, idx) => (
|
|
<SortableItem
|
|
key={dataEntry[selectedData.labelField] || `entry-${idx}`}
|
|
value={dataEntry[selectedData.labelField] || `Entry ${idx + 1}`}
|
|
index={idx}
|
|
crntIndex={idx}
|
|
selectedIndex={selectedIndex}
|
|
onSelectedIndexChange={(index: number) => setSelectedIndex(index)}
|
|
onDeleteItem={deleteItem}
|
|
/>
|
|
))}
|
|
</Container>
|
|
<Button className="mt-4" onClick={() => setSelectedIndex(null)}>
|
|
{l10n.t(LocalizationKey.dashboardDataViewDataViewAdd)}
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<div className={`flex flex-col items-center justify-center`}>
|
|
<p className={getColors(`text-gray-500 dark:text-whisper-900`, `text-[var(--frontmatter-text)]`)}>
|
|
{l10n.t(LocalizationKey.dashboardDataViewDataViewEmpty, selectedData.title.toLowerCase())}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div
|
|
className={`${selectedData.singleEntry ? 'w-full' : 'w-2/3'
|
|
} py-6 px-4 overflow-auto`}
|
|
>
|
|
<h2 className={`text-lg ${getColors(`text-gray-500 dark:text-whisper-900`, `text-[var(--frontmatter-text)]`)}`}>
|
|
{l10n.t(LocalizationKey.dashboardDataViewDataViewCreateOrModify, selectedData.title.toLowerCase())}
|
|
</h2>
|
|
{selectedData ? (
|
|
<DataForm
|
|
schema={selectedData?.schema}
|
|
model={dataEntry}
|
|
onSubmit={onSubmit}
|
|
onClear={() => setSelectedIndex(null)}
|
|
/>
|
|
) : (
|
|
<p>{l10n.t(LocalizationKey.dashboardDataViewDataViewGetStarted)}</p>
|
|
)}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<EmptyView />
|
|
)}
|
|
</section>
|
|
</div>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<div className={`flex flex-col items-center ${getColors(
|
|
'text-gray-500 dark:text-whisper-900',
|
|
'text-[var(--frontmatter-text)]'
|
|
)
|
|
}`}>
|
|
<DatabaseIcon className="w-32 h-32" />
|
|
<p className="text-3xl mt-2">{l10n.t(LocalizationKey.dashboardDataViewDataViewNoDataFiles)}</p>
|
|
<p className="text-xl mt-4">
|
|
<a
|
|
className={getColors(`text-teal-700 hover:text-teal-900`, `text-[var(--frontmatter-link)] hover:text-[var(--frontmatter-link-hover)]`)}
|
|
href={`https://frontmatter.codes/docs/dashboard#data-files-view`}
|
|
title={l10n.t(LocalizationKey.dashboardDataViewDataViewGetStartedLink)}
|
|
>
|
|
{l10n.t(LocalizationKey.dashboardDataViewDataViewGetStartedLink)}
|
|
</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
<SponsorMsg
|
|
beta={settings?.beta}
|
|
version={settings?.versionInfo}
|
|
isBacker={settings?.isBacker}
|
|
/>
|
|
|
|
<ToastContainer />
|
|
|
|
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=DataView" alt="DataView metrics" />
|
|
</div >
|
|
);
|
|
};
|