Notebook-widget (#962)

* ⬆️ Add required dependencies

*  Add basic widget definition for `notebook`

* 🌐 Add basic translations for `notebook` widget

* 🐛 Fix `WidgetMenu` zIndex property

* ️ Use dynamic import for the `notebook` widget

* 🌐 Update translations

* 🚨 Disable eslint `no-param-reassign` rule

*  Add `notebook` widget

*  Add `immer` as a dependency

* fix: currentConfig not loaded in useEffect callback 

fixes #1249

* ♻️ Notebook widget UI (#1266)

* ♻️ Refactor note widget

* 🐛 Fix translations

* 💄 Widget styling changes

* 🔒 Fix lockfile

* 💄 Remove primary color from edit button

* 💄 Fix css

*  Add the ability to hide an option

* 🔥 Remove aria-labels

* ♻️ Address pull request feedback

* 🐛 Remove wrong description from default value

---------

Co-authored-by: gnattu <gnattu@users.noreply.github.com>
Co-authored-by: Manuel <manuel.ruwe@bluewin.ch>
Co-authored-by: Tagaishi <Tagaishi@hotmail.ch>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Thomas Camlong
2023-08-12 21:17:12 +02:00
committed by GitHub
parent 7614ec25c3
commit abb52b093a
12 changed files with 1435 additions and 1370 deletions

View File

@@ -45,7 +45,7 @@
"@nivo/core": "^0.83.0",
"@nivo/line": "^0.83.0",
"@react-native-async-storage/async-storage": "^1.18.1",
"@tabler/icons-react": "^2.18.0",
"@tabler/icons-react": "^2.20.0",
"@tanstack/query-async-storage-persister": "^4.27.1",
"@tanstack/query-sync-storage-persister": "^4.27.1",
"@tanstack/react-query": "^4.2.1",
@@ -70,6 +70,7 @@
"geo-tz": "^7.0.7",
"html-entities": "^2.3.3",
"i18next": "^22.5.1",
"immer": "^10.0.2",
"js-file-download": "^0.4.12",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",

View File

@@ -0,0 +1,15 @@
{
"descriptor": {
"name": "Notebook",
"description": "A markdown-based interactive widget for you to write down your notes!",
"settings": {
"title": "Settings for the notebook widget",
"showToolbar": {
"label": "Show the toolbar to help you write markdown"
},
"content": {
"label": "The content of the notebook"
}
}
}
}

View File

@@ -2,6 +2,7 @@ import { ActionIcon, Menu } from '@mantine/core';
import { IconLayoutKanban, IconPencil, IconSettings, IconTrash } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useColorTheme } from '../../../tools/color';
import { useEditModeStore } from '../Views/useEditModeStore';
interface GenericTileMenuProps {
@@ -11,12 +12,14 @@ interface GenericTileMenuProps {
displayEdit: boolean;
}
export const GenericTileMenu = ({
handleClickEdit,
handleClickChangePosition,
handleClickDelete,
displayEdit,
}: GenericTileMenuProps) => {
export const GenericTileMenu = (
{
handleClickEdit,
handleClickChangePosition,
handleClickDelete,
displayEdit,
}: GenericTileMenuProps
) => {
const { t } = useTranslation('common');
const isEditMode = useEditModeStore((x) => x.enabled);
@@ -28,13 +31,13 @@ export const GenericTileMenu = ({
<Menu withinPortal withArrow position="right">
<Menu.Target>
<ActionIcon
style={{ zIndex: 1 }}
size="md"
radius="md"
variant="light"
pos="absolute"
top={8}
right={8}
style={{ zIndex: 1 }}
>
<IconSettings />
</ActionIcon>

View File

@@ -138,6 +138,8 @@ const WidgetOptionTypeSwitch: FC<{
const info = option.info ?? false;
const link = option.infoLink ?? undefined;
if (option.hide) return null;
switch (option.type) {
case 'switch':
return (

View File

@@ -15,6 +15,7 @@ import { rssRouter } from './routers/rss';
import { timezoneRouter } from './routers/timezone';
import { usenetRouter } from './routers/usenet/router';
import { weatherRouter } from './routers/weather';
import { notebookRouter } from './routers/notebook';
/**
* This is the primary router for your server.
@@ -37,6 +38,7 @@ export const rootRouter = createTRPCRouter({
timezone: timezoneRouter,
usenet: usenetRouter,
weather: weatherRouter,
notebook: notebookRouter
});
// export type definition of API

View File

@@ -0,0 +1,37 @@
import { TRPCError } from '@trpc/server';
import fs from 'fs';
import path from 'path';
import { z } from 'zod';
import { getConfig } from '~/tools/config/getConfig';
import { BackendConfigType } from '~/types/config';
import { INotebookWidget } from '~/widgets/notebook/NotebookWidgetTile';
import { createTRPCRouter, publicProcedure } from '../trpc';
export const notebookRouter = createTRPCRouter({
update: publicProcedure
.input(z.object({ widgetId: z.string(), content: z.string(), configName: z.string() }))
.mutation(async ({ input }) => {
const config = getConfig(input.configName);
const widget = config.widgets.find((widget) => widget.id === input.widgetId) as
| INotebookWidget
| undefined;
if (!widget) {
return new TRPCError({
code: 'BAD_REQUEST',
message: 'Specified widget was not found',
});
}
widget.properties.content = input.content;
const newConfig: BackendConfigType = {
...config,
widgets: [...config.widgets.filter((w) => w.id !== widget.id), widget],
};
const targetPath = path.join('data/configs', `${input.configName}.json`);
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
}),
});

View File

@@ -43,6 +43,7 @@ export const dashboardNamespaces = [
'modules/dns-hole-summary',
'modules/dns-hole-controls',
'modules/bookmark',
'modules/notebook',
'widgets/error-boundary',
'widgets/draggable-list',
'widgets/location',

View File

@@ -14,6 +14,7 @@ import torrent from './torrent/TorrentTile';
import usenet from './useNet/UseNetTile';
import videoStream from './video/VideoStreamTile';
import weather from './weather/WeatherTile';
import notebook from './notebook/NotebookWidgetTile';
export default {
calendar,
@@ -32,4 +33,5 @@ export default {
'dns-hole-summary': dnsHoleSummary,
'dns-hole-controls': dnsHoleControls,
bookmark,
notebook,
};

View File

@@ -0,0 +1,163 @@
import { ActionIcon, createStyles, rem } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { Link, RichTextEditor } from '@mantine/tiptap';
import { IconArrowUp, IconEdit, IconEditOff } from '@tabler/icons-react';
import { BubbleMenu, useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useEffect, useRef, useState } from 'react';
import { useConfigStore } from '~/config/store';
import { useColorTheme } from '~/tools/color';
import { api } from '~/utils/api';
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
import { useConfigContext } from '../../config/provider';
import { WidgetLoading } from '../loading';
import { INotebookWidget } from './NotebookWidgetTile';
Link.configure({
openOnClick: true,
});
export function Editor({ widget }: { widget: INotebookWidget }) {
const [content, setContent] = useState(widget.properties.content);
const { enabled } = useEditModeStore();
const [isEditing, setIsEditing] = useState(false);
const { config, name: configName } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig);
const { primaryColor } = useColorTheme();
const { mutateAsync } = api.notebook.update.useMutation();
const [debouncedContent] = useDebouncedValue(content, 500);
const editor = useEditor({
extensions: [StarterKit, Link],
content,
editable: false,
onUpdate: (e) => {
setContent(e.editor.getHTML());
},
});
const handleEditToggle = (previous: boolean) => {
const current = !previous;
if (!editor) return current;
editor.setEditable(current);
updateConfig(
configName!,
(previous) => {
const currentWidget = previous.widgets.find((x) => x.id === widget.id);
currentWidget!.properties.content = debouncedContent;
return {
...previous,
widgets: [
...previous.widgets.filter((iterationWidget) => iterationWidget.id !== widget.id),
currentWidget!,
],
};
},
true
);
void mutateAsync({
configName: configName!,
content: debouncedContent,
widgetId: widget.id,
});
return current;
};
if (!config || !configName) return <WidgetLoading />;
return (
<>
{!enabled && (
<ActionIcon
style={{
zIndex: 1,
}}
top={7}
right={7}
pos="absolute"
color={primaryColor}
variant="light"
size={30}
radius={'md'}
onClick={() => setIsEditing(handleEditToggle)}
>
{isEditing ? <IconEditOff size={20} /> : <IconEdit size={20} />}
</ActionIcon>
)}
<RichTextEditor
p={0}
mt={0}
editor={editor}
styles={(theme) => ({
root: {
'& .ProseMirror': {
padding: '0 !important',
},
border: 'none',
},
toolbar: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
paddingTop: 0,
paddingBottom: theme.spacing.md,
},
content: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
},
})}
>
<RichTextEditor.Toolbar
style={{
display: isEditing && widget.properties.showToolbar === true ? 'flex' : 'none',
}}
>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
{editor && (
<BubbleMenu editor={editor}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Link />
</RichTextEditor.ControlsGroup>
</BubbleMenu>
)}
<RichTextEditor.Content />
</RichTextEditor>
</>
);
}

View File

@@ -0,0 +1,45 @@
import { IconNotes } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
const Editor = dynamic(() => import('./NotebookEditor').then((module) => module.Editor), {
ssr: false,
});
const definition = defineWidget({
id: 'notebook',
icon: IconNotes,
options: {
showToolbar: {
type: 'switch',
defaultValue: true,
},
content: {
type: 'text',
hide: true,
defaultValue: `<h2>Welcome to <strong>Homarr's</strong> notebook widget</h2><p>The <code>notebook</code> widget focuses on usability and is designed to be as simple as possible to bring a familiar editing experience to regular users. It is based on <a target="_blank" rel="noopener noreferrer nofollow" href="https://tiptap.dev/">Tiptap.dev</a> and supports all of its features:</p><ul><li><p>General text formatting: <strong>bold</strong>, <em>italic</em>, underline, <s>strike-through</s></p></li><li><p>Headings (h1-h6)</p></li><li><p>Sub and super scripts (&lt;sup /&gt; and &lt;sub /&gt; tags)</p></li><li><p>Ordered and bullet lists</p></li><li><p>Text align&nbsp;</p></li></ul><h3>Widget options</h3><p>This widget has two options :</p><ul><li><p>Show toolbar : Shows the toolbar when the widget is in the local edit mode.</p></li></ul>`,
},
},
gridstack: {
minWidth: 3,
minHeight: 2,
maxWidth: 12,
maxHeight: 12,
},
component: NotebookWidget,
});
export default definition;
export type INotebookWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface NotebookWidgetProps {
widget: INotebookWidget;
}
function NotebookWidget(props: NotebookWidgetProps) {
return <Editor widget={props.widget} />;
}

View File

@@ -53,6 +53,7 @@ interface DataType {
interface ICommonWidgetOptions {
info?: boolean;
hide?: boolean;
infoLink?: string;
}

2517
yarn.lock

File diff suppressed because it is too large Load Diff