🔀 Merge pull request #543 from manuel-rw/gridstack

This commit is contained in:
Manuel
2022-12-24 15:48:29 +01:00
committed by GitHub
12 changed files with 129 additions and 120 deletions

View File

@@ -14,6 +14,7 @@ import { useState } from 'react';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { AppType } from '../../../../types/app';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab';
import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab';
import { GeneralTab } from './Tabs/GeneralTab/GeneralTab';
@@ -33,6 +34,7 @@ export const EditAppModal = ({
const { t } = useTranslation(['layout/modals/add-app', 'common']);
const { name: configName, config } = useConfigContext();
const updateConfig = useConfigStore((store) => store.updateConfig);
const { enabled: isEditMode } = useEditModeStore();
const [allowAppNamePropagation, setAllowAppNamePropagation] = useState<boolean>(
innerProps.allowAppNamePropagation
);
@@ -87,9 +89,15 @@ export const EditAppModal = ({
configName,
(previousConfig) => ({
...previousConfig,
apps: [...previousConfig.apps.filter((x) => x.id !== form.values.id), form.values],
apps: [
...previousConfig.apps.filter((x) => x.id !== values.id),
{
...values,
},
],
}),
true
true,
!isEditMode
);
// also close the parent modal

View File

@@ -3,8 +3,10 @@ import { IconBox, IconStack } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { ReactNode } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { useConfigContext } from '../../../../../../config/provider';
import { openContextModalGeneric } from '../../../../../../tools/mantineModalManagerExtensions';
import { AppType } from '../../../../../../types/app';
import { appTileDefinition } from '../../../../Tiles/Apps/AppTile';
import { useStyles } from '../Shared/styles';
interface AvailableElementTypesProps {
@@ -17,6 +19,8 @@ export const AvailableElementTypes = ({
onOpenStaticElements,
}: AvailableElementTypesProps) => {
const { t } = useTranslation('layout/element-selector/selector');
const { config } = useConfigContext();
const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0];
return (
<>
@@ -45,10 +49,11 @@ export const AvailableElementTypes = ({
isOpeningNewTab: true,
externalUrl: '',
},
area: {
type: 'sidebar',
type: 'wrapper',
properties: {
location: 'right',
id: getLowestWrapper()?.id ?? '',
},
},
shape: {
@@ -57,8 +62,8 @@ export const AvailableElementTypes = ({
y: 0,
},
size: {
height: 1,
width: 1,
width: appTileDefinition.minWidth,
height: appTileDefinition.minHeight,
},
},
integration: {

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'next-i18next';
import { useConfigContext } from '../../../../../../config/provider';
import { useConfigStore } from '../../../../../../config/store';
import { IWidget, IWidgetDefinition } from '../../../../../../widgets/widgets';
import { useEditModeStore } from '../../../../Views/useEditModeStore';
import { GenericAvailableElementType } from '../Shared/GenericElementType';
interface WidgetElementTypeProps {
@@ -18,6 +19,7 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement
const { t } = useTranslation(`modules/${id}`);
const { name: configName, config } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig);
const isEditMode = useEditModeStore((x) => x.enabled);
if (!configName) return null;
@@ -56,9 +58,10 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement
},
],
}),
true
true,
!isEditMode
);
// TODO: safe to file system
closeModal('selectElement');
};

View File

@@ -88,3 +88,11 @@ const useStyles = createStyles((theme, _params, getRef) => ({
},
},
}));
export const appTileDefinition = {
component: AppTile,
minWidth: 2,
maxWidth: 12,
minHeight: 2,
maxHeight: 12,
};

View File

@@ -1,77 +0,0 @@
import calendarDefinition from '../../../widgets/calendar/CalendarTile';
import clockDefinition from '../../../widgets/date/DateTile';
import dashDotDefinition from '../../../widgets/dashDot/DashDotTile';
import useNetDefinition from '../../../widgets/useNet/UseNetTile';
import weatherDefinition from '../../../widgets/weather/WeatherTile';
import { EmptyTile } from './EmptyTile';
import { AppTile } from './Apps/AppTile';
// TODO: just remove and use app (later app) directly. For widgets the the definition should contain min/max width/height
type TileDefinitionProps = {
[key in keyof any | 'app']: {
minWidth?: number;
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
component: React.ElementType;
};
};
export const Tiles: TileDefinitionProps = {
app: {
component: AppTile,
minWidth: 2,
maxWidth: 12,
minHeight: 2,
maxHeight: 12,
},
bitTorrent: {
component: EmptyTile,
minWidth: 4,
maxWidth: 12,
minHeight: 5,
maxHeight: 12,
},
calendar: {
component: calendarDefinition.component,
minWidth: 4,
maxWidth: 12,
minHeight: 5,
maxHeight: 12,
},
clock: {
component: clockDefinition.component,
minWidth: 4,
maxWidth: 12,
minHeight: 2,
maxHeight: 12,
},
dashDot: {
component: dashDotDefinition.component,
minWidth: 4,
maxWidth: 9,
minHeight: 5,
maxHeight: 14,
},
torrentNetworkTraffic: {
component: EmptyTile,
minWidth: 4,
maxWidth: 12,
minHeight: 5,
maxHeight: 12,
},
useNet: {
component: useNetDefinition.component,
minWidth: 4,
maxWidth: 12,
minHeight: 5,
maxHeight: 12,
},
weather: {
component: weatherDefinition.component,
minWidth: 4,
maxWidth: 12,
minHeight: 2,
maxHeight: 12,
},
};

View File

@@ -4,7 +4,7 @@ import { AppType } from '../../../types/app';
import Widgets from '../../../widgets';
import { IWidget, IWidgetDefinition } from '../../../widgets/widgets';
import { WidgetWrapper } from '../../../widgets/WidgetWrapper';
import { Tiles } from '../Tiles/tilesDefinitions';
import { appTileDefinition } from '../Tiles/Apps/AppTile';
import { GridstackTileWrapper } from '../Tiles/TileWrapper';
interface WrapperContentProps {
@@ -21,7 +21,7 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) {
return (
<>
{apps?.map((app) => {
const { component: TileComponent, ...tile } = Tiles.app;
const { component: TileComponent, ...tile } = appTileDefinition;
return (
<GridstackTileWrapper
id={app.id}

View File

@@ -1,35 +1,53 @@
import { Stack } from '@mantine/core';
import { Button, ScrollArea, Stack } from '@mantine/core';
import { useConfigContext } from '../../../config/provider';
import { ColorSelector } from './Theme/ColorSelector';
import { useConfigStore } from '../../../config/store';
import { LayoutSelector } from './Layout/LayoutSelector';
import { BackgroundChanger } from './Meta/BackgroundChanger';
import { CustomCssChanger } from './Theme/CustomCssChanger';
import { FaviconChanger } from './Meta/FaviconChanger';
import { LogoImageChanger } from './Meta/LogoImageChanger';
import { MetaTitleChanger } from './Meta/MetaTitleChanger';
import { PageTitleChanger } from './Meta/PageTitleChanger';
import { ColorSelector } from './Theme/ColorSelector';
import { CustomCssChanger } from './Theme/CustomCssChanger';
import { OpacitySelector } from './Theme/OpacitySelector';
import { ShadeSelector } from './Theme/ShadeSelector';
import { LayoutSelector } from './Layout/LayoutSelector';
export default function CustomizationSettings() {
const { config } = useConfigContext();
const { config, name: configName } = useConfigContext();
const { updateConfig } = useConfigStore();
const saveConfiguration = () => {
if (!configName || !config) {
return;
}
updateConfig(configName, (_) => config, false, true);
};
return (
<Stack mb="md" mr="sm" mt="xs">
<LayoutSelector defaultLayout={config?.settings.customization.layout} />
<PageTitleChanger defaultValue={config?.settings.customization.pageTitle} />
<MetaTitleChanger defaultValue={config?.settings.customization.metaTitle} />
<LogoImageChanger defaultValue={config?.settings.customization.logoImageUrl} />
<FaviconChanger defaultValue={config?.settings.customization.faviconUrl} />
<BackgroundChanger defaultValue={config?.settings.customization.backgroundImageUrl} />
<CustomCssChanger defaultValue={config?.settings.customization.customCss} />
<ColorSelector type="primary" defaultValue={config?.settings.customization.colors.primary} />
<ColorSelector
type="secondary"
defaultValue={config?.settings.customization.colors.secondary}
/>
<ShadeSelector defaultValue={config?.settings.customization.colors.shade} />
<OpacitySelector defaultValue={config?.settings.customization.appOpacity} />
<ScrollArea style={{ height: '76vh' }} offsetScrollbars>
<LayoutSelector defaultLayout={config?.settings.customization.layout} />
<PageTitleChanger defaultValue={config?.settings.customization.pageTitle} />
<MetaTitleChanger defaultValue={config?.settings.customization.metaTitle} />
<LogoImageChanger defaultValue={config?.settings.customization.logoImageUrl} />
<FaviconChanger defaultValue={config?.settings.customization.faviconUrl} />
<BackgroundChanger defaultValue={config?.settings.customization.backgroundImageUrl} />
<CustomCssChanger defaultValue={config?.settings.customization.customCss} />
<ColorSelector
type="primary"
defaultValue={config?.settings.customization.colors.primary}
/>
<ColorSelector
type="secondary"
defaultValue={config?.settings.customization.colors.secondary}
/>
<ShadeSelector defaultValue={config?.settings.customization.colors.shade} />
<OpacitySelector defaultValue={config?.settings.customization.appOpacity} />
</ScrollArea>
<Button onClick={saveConfiguration} variant="light">Save Customizations</Button>
</Stack>
);
}

View File

@@ -1,9 +1,9 @@
import { Title, Drawer, Tabs, ScrollArea } from '@mantine/core';
import { Drawer, ScrollArea, Tabs, Title } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import CustomizationSettings from './Customization/CustomizationSettings';
import CommonSettings from './Common/CommonSettings';
import Credits from './Common/Credits';
import CustomizationSettings from './Customization/CustomizationSettings';
function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
const { t } = useTranslation('settings/common');
@@ -20,9 +20,7 @@ function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string })
</ScrollArea>
</Tabs.Panel>
<Tabs.Panel value="customization">
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
<CustomizationSettings />
</ScrollArea>
<CustomizationSettings />
</Tabs.Panel>
</Tabs>
);

View File

@@ -1,3 +1,4 @@
import axios from 'axios';
import create from 'zustand';
import { ConfigType } from '../types/config';
@@ -15,7 +16,8 @@ export const useConfigStore = create<UseConfigStoreType>((set, get) => ({
updateConfig: async (
name,
updateCallback: (previous: ConfigType) => ConfigType,
shouldRegenerateGridstack = false
shouldRegenerateGridstack = false,
shouldSaveConfigToFileSystem = false
) => {
const { configs } = get();
const currentConfig = configs.find((x) => x.value.configProperties.name === name);
@@ -23,7 +25,6 @@ export const useConfigStore = create<UseConfigStoreType>((set, get) => ({
// copies the value of currentConfig and creates a non reference object named previousConfig
const previousConfig: ConfigType = JSON.parse(JSON.stringify(currentConfig.value));
// TODO: update config on server
const updatedConfig = updateCallback(currentConfig.value);
set((old) => ({
...old,
@@ -40,6 +41,10 @@ export const useConfigStore = create<UseConfigStoreType>((set, get) => ({
) {
currentConfig.increaseVersion();
}
if (shouldSaveConfigToFileSystem) {
axios.put(`/api/configs/${name}`, { ...updatedConfig });
}
},
}));
@@ -51,6 +56,7 @@ interface UseConfigStoreType {
updateCallback: (previous: ConfigType) => ConfigType,
shouldRegenerateGridstace?:
| boolean
| ((previousConfig: ConfigType, currentConfig: ConfigType) => boolean)
| ((previousConfig: ConfigType, currentConfig: ConfigType) => boolean),
shouldSaveConfigToFileSystem?: boolean
) => Promise<void>;
}

View File

@@ -5,6 +5,7 @@
import { TablerIcon } from '@tabler/icons';
// Note: Maybe use context to keep track of the modules
// TODO: Remove this old component and the entire file
export interface IModule {
id: string;
title: string;

View File

@@ -1,22 +1,61 @@
import { NextApiRequest, NextApiResponse } from 'next';
import fs from 'fs';
import path from 'path';
import { BackendConfigType, ConfigType } from '../../../types/config';
import { getConfig } from '../../../tools/config/getConfig';
function Put(req: NextApiRequest, res: NextApiResponse) {
// Get the slug of the request
const { slug } = req.query as { slug: string };
// Get the body of the request
const { body }: { body: string } = req;
if (!slug || !body) {
res.status(400).json({
const { body: config }: { body: ConfigType } = req;
if (!slug || !config) {
return res.status(400).json({
error: 'Wrong request',
});
}
// Save the body in the /data/config folder with the slug as filename
const previousConfig = getConfig(slug);
const newConfig: BackendConfigType = {
...config,
apps: [
...config.apps.map((app) => ({
...app,
integration: {
...app.integration,
properties: app.integration.properties.map((property) => {
if (property.type === 'public') {
return {
field: property.field,
type: property.type,
value: property.value,
};
}
const previousApp = previousConfig.apps.find(
(previousApp) => previousApp.id === app.id
);
const previousProperty = previousApp?.integration?.properties.find(
(previousProperty) => previousProperty.field === property.field
);
return {
field: property.field,
type: property.type,
value: property.value !== undefined ? property.value : previousProperty?.value,
};
}),
},
})),
],
};
// Save the body in the /data/config folder with the slug as filename
fs.writeFileSync(
path.join('data/configs', `${slug}.json`),
JSON.stringify(body, null, 2),
JSON.stringify(newConfig, null, 2),
'utf8'
);
return res.status(200).json({

View File

@@ -9,7 +9,7 @@
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"forceConsistentCasingInFileNames": false,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",