From 98007f37761ca6fe66a7f6e9db77a38d8b7e52d3 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Wed, 11 Oct 2023 22:03:55 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Add=20edit=20app=20action?= =?UTF-8?q?=20for=20new=20board?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Board/Items/App/AppItem.tsx | 6 +- src/components/Board/Items/App/AppMenu.tsx | 8 +- .../Board/Items/App/Edit/AppereanceTab.tsx | 69 +++------ .../Board/Items/App/Edit/BehaviourTab.tsx | 22 ++- .../Board/Items/App/Edit/GeneralTab.tsx | 21 +-- .../Edit/IntegrationTab/IntegrationTab.tsx | 8 +- .../Board/Items/App/Edit/NetworkTab.tsx | 25 +-- .../Board/Items/App/EditAppModal.tsx | 145 ++++++++---------- .../Board/Items/App/app-actions.tsx | 54 +++++++ .../Board/Sections/EmptySection.tsx | 1 - .../Overview/AvailableElementsOverview.tsx | 61 +++----- .../SelectElement/SelectElementModal.tsx | 41 +++-- .../AvailableStaticElementsTab.tsx | 33 ---- .../Board/gridstack/init-gridstack.ts | 2 +- .../layout/Templates/BoardLayout.tsx | 5 +- src/server/db/items.ts | 6 +- src/server/db/schema.ts | 4 +- src/tools/shared/app.ts | 22 ++- 18 files changed, 251 insertions(+), 282 deletions(-) create mode 100644 src/components/Board/Items/App/app-actions.tsx delete mode 100644 src/components/Board/SelectElement/StaticElementsTab/AvailableStaticElementsTab.tsx diff --git a/src/components/Board/Items/App/AppItem.tsx b/src/components/Board/Items/App/AppItem.tsx index 0cb5a3085..bc0eb5e17 100644 --- a/src/components/Board/Items/App/AppItem.tsx +++ b/src/components/Board/Items/App/AppItem.tsx @@ -9,7 +9,7 @@ import { ItemWrapper } from '../ItemWrapper'; import { AppMenu } from './AppMenu'; import { AppPing } from './AppPing'; -interface AppTileProps { +interface AppItemProps { app: AppItem; className?: string; } @@ -21,7 +21,7 @@ const namePositions = { bottom: 'column-reverse', }; -export const BoardAppItem = ({ className, app }: AppTileProps) => { +export const BoardAppItem = ({ className, app }: AppItemProps) => { const isEditMode = useEditModeStore((x) => x.enabled); const { cx, classes } = useStyles(); const { colorScheme } = useMantineTheme(); @@ -49,7 +49,7 @@ export const BoardAppItem = ({ className, app }: AppTileProps) => { flexFlow: namePositions[app.namePosition] ?? 'column', }} > - {app.nameStyle === 'show' && ( + {app.nameStyle === 'normal' && ( { +export const AppMenu = ({ app }: AppMenuProps) => { const board = useRequiredBoard(); const { removeItem } = useItemActions({ boardName: board.name }); const resizeGridItem = useResizeGridItem(); const handleClickEdit = () => { - openContextModalGeneric<{ app: AppItem; allowAppNamePropagation: boolean }>({ + openContextModalGeneric({ modal: 'editApp', size: 'xl', innerProps: { app, + board, allowAppNamePropagation: false, }, styles: { diff --git a/src/components/Board/Items/App/Edit/AppereanceTab.tsx b/src/components/Board/Items/App/Edit/AppereanceTab.tsx index dd9aac3eb..3752fa055 100644 --- a/src/components/Board/Items/App/Edit/AppereanceTab.tsx +++ b/src/components/Board/Items/App/Edit/AppereanceTab.tsx @@ -1,14 +1,14 @@ -import { Flex, NumberInput, Select, Stack, Switch, Tabs } from '@mantine/core'; -import { UseFormReturnType } from '@mantine/form'; +import { Flex, NumberInput, Select, Stack, Tabs } from '@mantine/core'; import { useDebouncedValue } from '@mantine/hooks'; import { useTranslation } from 'next-i18next'; import { useEffect, useRef } from 'react'; - -import { AppType } from '~/types/app'; import { IconSelector } from '~/components/IconSelector/IconSelector'; +import { appNamePositions, appNameStyles } from '~/server/db/items'; + +import { AppForm } from '../EditAppModal'; interface AppearanceTabProps { - form: UseFormReturnType AppType>; + form: AppForm; disallowAppNamePropagation: () => void; allowAppNamePropagation: boolean; } @@ -43,72 +43,47 @@ export const AppearanceTab = ({ { - form.setFieldValue('appearance.iconUrl', value); + form.setFieldValue('iconUrl', value!); disallowAppNamePropagation(); }} - value={form.values.appearance.iconUrl} + value={form.values.iconUrl} ref={iconSelectorRef} /> { - form.setFieldValue('appearance.positionAppName', value); - }} + data={appNamePositions.map((value) => ({ + value, + label: t(`appearance.positionAppName.dropdown.${value}`)!, + }))} + {...form.getInputProps('namePosition')} /> { - form.setFieldValue('appearance.lineClampAppName', value); - }} + {...form.getInputProps('nameLineClamp')} /> )} @@ -116,5 +91,3 @@ export const AppearanceTab = ({ ); }; - -const replaceCharacters = (value: string) => value.toLowerCase().replaceAll('', '-'); diff --git a/src/components/Board/Items/App/Edit/BehaviourTab.tsx b/src/components/Board/Items/App/Edit/BehaviourTab.tsx index 3280c6e15..af9341e9b 100644 --- a/src/components/Board/Items/App/Edit/BehaviourTab.tsx +++ b/src/components/Board/Items/App/Edit/BehaviourTab.tsx @@ -1,17 +1,15 @@ -import { Text, TextInput, Tooltip, Stack, Switch, Tabs, Group, useMantineTheme, HoverCard } from '@mantine/core'; -import { UseFormReturnType } from '@mantine/form'; +import { Group, Stack, Switch, Tabs, Text, TextInput, useMantineTheme } from '@mantine/core'; import { useTranslation } from 'next-i18next'; +import { InfoCard } from '~/components/InfoCard/InfoCard'; -import { AppType } from '~/types/app'; -import { InfoCard } from '~/components/InfoCard/InfoCard' +import { AppForm } from '../EditAppModal'; interface BehaviourTabProps { - form: UseFormReturnType AppType>; + form: AppForm; } export const BehaviourTab = ({ form }: BehaviourTabProps) => { const { t } = useTranslation('layout/modals/add-app'); - const { primaryColor } = useMantineTheme(); return ( @@ -19,21 +17,19 @@ export const BehaviourTab = ({ form }: BehaviourTabProps) => { {t('behaviour.tooltipDescription.label')} - + - + ); -}; \ No newline at end of file +}; diff --git a/src/components/Board/Items/App/Edit/GeneralTab.tsx b/src/components/Board/Items/App/Edit/GeneralTab.tsx index 2fef44a43..0c4ed8c28 100644 --- a/src/components/Board/Items/App/Edit/GeneralTab.tsx +++ b/src/components/Board/Items/App/Edit/GeneralTab.tsx @@ -1,17 +1,14 @@ import { Stack, Tabs, Text, TextInput } from '@mantine/core'; -import { UseFormReturnType } from '@mantine/form'; import { IconClick, IconCursorText, IconLink } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; -import { AppType } from '~/types/app'; -import { EditAppModalTab } from '../EditAppModal'; +import { AppForm } from '../EditAppModal'; interface GeneralTabProps { - form: UseFormReturnType AppType>; - openTab: (tab: EditAppModalTab) => void; + form: AppForm; } -export const GeneralTab = ({ form, openTab }: GeneralTabProps) => { +export const GeneralTab = ({ form }: GeneralTabProps) => { const { t } = useTranslation('layout/modals/add-app'); return ( @@ -32,10 +29,7 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => { placeholder="https://google.com" variant="default" withAsterisk - {...form.getInputProps('url')} - onChange={(e) => { - form.setFieldValue('url', e.target.value); - }} + {...form.getInputProps('internalUrl')} /> } @@ -43,11 +37,12 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => { description={t('general.externalAddress.description')} placeholder="https://homarr.mywebsite.com/" variant="default" - {...form.getInputProps('behaviour.externalUrl')} + {...form.getInputProps('externalUrl')} /> - {!form.values.behaviour.externalUrl.startsWith('https://') && - !form.values.behaviour.externalUrl.startsWith('http://') && ( + {form.values.externalUrl && + !form.values.externalUrl.startsWith('https://') && + !form.values.externalUrl.startsWith('http://') && ( {t('behaviour.customProtocolWarning')} diff --git a/src/components/Board/Items/App/Edit/IntegrationTab/IntegrationTab.tsx b/src/components/Board/Items/App/Edit/IntegrationTab/IntegrationTab.tsx index 188ad9a9f..2ff75acb0 100644 --- a/src/components/Board/Items/App/Edit/IntegrationTab/IntegrationTab.tsx +++ b/src/components/Board/Items/App/Edit/IntegrationTab/IntegrationTab.tsx @@ -4,16 +4,18 @@ import { IconAlertTriangle } from '@tabler/icons-react'; import { Trans, useTranslation } from 'next-i18next'; import { AppType } from '~/types/app'; +import { AppForm } from '../../EditAppModal'; import { IntegrationSelector } from './InputElements/IntegrationSelector'; import { IntegrationOptionsRenderer } from './IntegrationOptionsRenderer/IntegrationOptionsRenderer'; interface IntegrationTabProps { - form: UseFormReturnType AppType>; + form: AppForm; } export const IntegrationTab = ({ form }: IntegrationTabProps) => { + return <>; /* const { t } = useTranslation('layout/modals/add-app'); - const hasIntegrationSelected = form.values.integration?.type; + const hasIntegrationSelected = form.values.integrationId?.type; return ( @@ -34,5 +36,5 @@ export const IntegrationTab = ({ form }: IntegrationTabProps) => { )} - ); + );*/ }; diff --git a/src/components/Board/Items/App/Edit/NetworkTab.tsx b/src/components/Board/Items/App/Edit/NetworkTab.tsx index 6b2aa809e..c514bdbdd 100644 --- a/src/components/Board/Items/App/Edit/NetworkTab.tsx +++ b/src/components/Board/Items/App/Edit/NetworkTab.tsx @@ -1,30 +1,27 @@ import { MultiSelect, Stack, Switch, Tabs } from '@mantine/core'; -import { UseFormReturnType } from '@mantine/form'; import { useTranslation } from 'next-i18next'; - import { StatusCodes } from '~/tools/acceptableStatusCodes'; -import { AppType } from '~/types/app'; + +import { AppForm } from '../EditAppModal'; interface NetworkTabProps { - form: UseFormReturnType AppType>; + form: AppForm; } export const NetworkTab = ({ form }: NetworkTabProps) => { const { t } = useTranslation('layout/modals/add-app'); - const acceptableStatusCodes = (form.values.network.statusCodes ?? ['200']).map((x) => - x.toString() - ); + const acceptableStatusCodes = (form.values.statusCodes ?? [200]).map((x) => x.toString()); return ( - {form.values.network.enabledStatusChecker && ( + {form.values.isPingEnabled && ( { searchable defaultValue={acceptableStatusCodes} variant="default" - {...form.getInputProps('network.statusCodes')} + {...form.getInputProps('statusCodes')} + value={form.getInputProps('statusCodes').value.map((x: number) => x.toString())} + onChange={(values) => { + form.getInputProps('statusCodes').onChange(values.map((x) => parseInt(x, 10))); + }} /> )} diff --git a/src/components/Board/Items/App/EditAppModal.tsx b/src/components/Board/Items/App/EditAppModal.tsx index fc608564d..7843a5acd 100644 --- a/src/components/Board/Items/App/EditAppModal.tsx +++ b/src/components/Board/Items/App/EditAppModal.tsx @@ -1,5 +1,5 @@ -import { Alert, Button, Group, Popover, Stack, Tabs, Text, ThemeIcon } from '@mantine/core'; -import { useForm } from '@mantine/form'; +import { Button, Group, Popover, Stack, Tabs, Text, ThemeIcon } from '@mantine/core'; +import { UseFormReturnType, useForm } from '@mantine/form'; import { useDisclosure } from '@mantine/hooks'; import { ContextModalProps } from '@mantine/modals'; import { @@ -12,17 +12,20 @@ import { } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; import { useState } from 'react'; -import { useConfigContext } from '~/config/provider'; -import { useConfigStore } from '~/config/store'; -import { AppType } from '~/types/app'; +import { z } from 'zod'; +import { appNamePositions, appNameStyles } from '~/server/db/items'; +import { objectKeys } from '~/tools/object'; +import { RouterOutputs } from '~/utils/api'; +import { useI18nZodResolver } from '~/utils/i18n-zod-resolver'; import { DebouncedImage } from '../../../IconSelector/DebouncedImage'; -import { useEditModeStore } from '../../useEditModeStore'; +import { AppItem } from '../../context'; import { AppearanceTab } from './Edit/AppereanceTab'; import { BehaviourTab } from './Edit/BehaviourTab'; import { GeneralTab } from './Edit/GeneralTab'; import { IntegrationTab } from './Edit/IntegrationTab/IntegrationTab'; import { NetworkTab } from './Edit/NetworkTab'; +import { useAppActions } from './app-actions'; const appUrlRegex = '(https?://(?:www.|(?!www))\\[?[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\]?.[^\\s]{2,}|www.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|https?://(?:www.|(?!www))\\[?[a-zA-Z0-9]+\\]?.[^\\s]{2,}|www.[a-zA-Z0-9]+.[^\\s]{2,})'; @@ -30,93 +33,47 @@ const appUrlRegex = const appUrlWithAnyProtocolRegex = '([A-z]+://(?:www.|(?!www))\\[?[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\]?.[^\\s]{2,}|www.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|[A-z]+://(?:www.|(?!www))\\[?[a-zA-Z0-9]+\\]?.[^\\s]{2,}|www.[a-zA-Z0-9]+.[^\\s]{2,})'; +export type EditAppModalInnerProps = { + app: AppItem; + board: RouterOutputs['boards']['byName']; + allowAppNamePropagation: boolean; +}; + export const EditAppModal = ({ context, id, innerProps, -}: ContextModalProps<{ app: AppType; allowAppNamePropagation: boolean }>) => { +}: ContextModalProps) => { const { t } = useTranslation(['layout/modals/add-app', 'common']); - const { name: configName, config } = useConfigContext(); - const updateConfig = useConfigStore((store) => store.updateConfig); - const { enabled: isEditMode } = useEditModeStore(); + const [activeTab, setActiveTab] = useState('general'); + const { createOrUpdateApp } = useAppActions({ boardName: innerProps.board.name }); + // TODO: change to ref const [allowAppNamePropagation, setAllowAppNamePropagation] = useState( innerProps.allowAppNamePropagation ); - const form = useForm({ + const { i18nZodResolver } = useI18nZodResolver(); + + const form = useForm({ initialValues: innerProps.app, - validate: { - name: (name) => (!name ? t('validation.name') : null), - url: (url) => { - if (!url) { - return t('validation.noUrl'); - } - - if (!url.match(appUrlRegex)) { - return t('validation.invalidUrl'); - } - - return null; - }, - appearance: { - iconUrl: (url: string) => { - if (url.length < 1) { - return t('validation.noIconUrl'); - } - - return null; - }, - }, - behaviour: { - externalUrl: (url: string) => { - if (url === undefined || url.length < 1) { - return t('validation.noExternalUri'); - } - - if (!url.match(appUrlWithAnyProtocolRegex)) { - return t('validation.invalidExternalUri'); - } - - return null; - }, - }, - }, + validate: i18nZodResolver(appFormSchema), validateInputOnChange: true, + validateInputOnBlur: true, }); - const onSubmit = (values: AppType) => { - if (!configName) { - return; - } - - updateConfig( - configName, - (previousConfig) => ({ - ...previousConfig, - apps: [ - ...previousConfig.apps.filter((x) => x.id !== values.id), - { - ...values, - }, - ], - }), - true, - !isEditMode - ); - + const onSubmit = (values: FormType) => { + createOrUpdateApp({ app: values }); // also close the parent modal context.closeAll(); }; - const [activeTab, setActiveTab] = useState('general'); - const closeModal = () => { context.closeModal(id); }; - const validationErrors = Object.keys(form.errors); + const validationErrors = objectKeys(form.errors) as (keyof AppItem)[]; - const ValidationErrorIndicator = ({ keys }: { keys: string[] }) => { + const ValidationErrorIndicator = ({ keys }: { keys: (keyof AppItem)[] }) => { const relevantErrors = validationErrors.filter((x) => keys.includes(x)); return ( @@ -133,15 +90,8 @@ export const EditAppModal = ({ return ( <> - {configName === undefined || - (config === undefined && ( - - There was an unexpected problem loading the configuration. Functionality might be - restricted. Please report this incident. - - ))} - + {form.values.name ?? 'New App'} @@ -163,28 +113,34 @@ export const EditAppModal = ({ > } + rightSection={ + + } icon={} value="general" > {t('tabs.general')} } + rightSection={} icon={} value="behaviour" > {t('tabs.behaviour')} } + rightSection={} icon={} value="network" > {t('tabs.network')} } + rightSection={ + + } icon={} value="appearance" > @@ -199,7 +155,7 @@ export const EditAppModal = ({ - setActiveTab(targetTab)} /> + { }; export type EditAppModalTab = 'general' | 'behaviour' | 'network' | 'appereance' | 'integration'; + +export const appFormSchema = z.object({ + id: z.string().nonempty(), + type: z.literal('app'), + name: z.string().min(2).max(64), + internalUrl: z.string().regex(new RegExp(appUrlRegex)), + externalUrl: z.string().regex(new RegExp(appUrlWithAnyProtocolRegex)).nullable(), + iconUrl: z.string().nonempty(), + nameStyle: z.enum(appNameStyles), + namePosition: z.enum(appNamePositions), + nameLineClamp: z.number().min(0).max(10), + fontSize: z.number().min(5).max(64), + isPingEnabled: z.boolean(), + statusCodes: z.array(z.number().min(100).max(999)), + openInNewTab: z.boolean(), + description: z.string().nonempty().max(512).nullable(), + integrationId: z.string().nonempty().nullable(), +}); + +type FormType = z.infer; +export type AppForm = UseFormReturnType FormType>; diff --git a/src/components/Board/Items/App/app-actions.tsx b/src/components/Board/Items/App/app-actions.tsx new file mode 100644 index 000000000..15be2fd20 --- /dev/null +++ b/src/components/Board/Items/App/app-actions.tsx @@ -0,0 +1,54 @@ +import { useCallback } from 'react'; +import { z } from 'zod'; +import { api } from '~/utils/api'; + +import { AppItem, EmptySection } from '../../context'; +import { appFormSchema } from './EditAppModal'; + +type CreateOrUpdateApp = { + app: z.infer; +}; + +export const useAppActions = ({ boardName }: { boardName: string }) => { + const utils = api.useContext(); + const createOrUpdateApp = useCallback( + ({ app }: CreateOrUpdateApp) => { + utils.boards.byName.setData({ boardName }, (prev) => { + if (!prev) return prev; + + let sectionId = prev.sections.find((section) => + section.items.some((item) => item.id === app.id) + )?.id; + + if (!sectionId) { + sectionId = prev.sections + .filter((section): section is EmptySection => section.type === 'empty') + .sort((a, b) => a.position - b.position)[0].id; + } + + return { + ...prev, + sections: prev.sections.map((section) => { + // Return same section if item is not in it + if (section.id !== sectionId) return section; + return { + ...section, + items: section.items.map((item) => { + // Return same item if item is not the one we're moving + if (item.id !== app.id) return item; + return { + ...(app as AppItem), + }; + }), + }; + }), + }; + }); + }, + [boardName, utils] + ); + + return { + createOrUpdateApp, + }; +}; diff --git a/src/components/Board/Sections/EmptySection.tsx b/src/components/Board/Sections/EmptySection.tsx index 6c3a7329a..b1f7f8cc1 100644 --- a/src/components/Board/Sections/EmptySection.tsx +++ b/src/components/Board/Sections/EmptySection.tsx @@ -27,7 +27,6 @@ export const BoardEmptySection = ({ section }: EmptySectionWrapperProps) => { data-empty={section.id} ref={refs.wrapper} > - {section.items.length === 0 && } diff --git a/src/components/Board/SelectElement/Overview/AvailableElementsOverview.tsx b/src/components/Board/SelectElement/Overview/AvailableElementsOverview.tsx index 601cd353c..63ab98e9c 100644 --- a/src/components/Board/SelectElement/Overview/AvailableElementsOverview.tsx +++ b/src/components/Board/SelectElement/Overview/AvailableElementsOverview.tsx @@ -7,31 +7,35 @@ import { useTranslation } from 'next-i18next'; import { ReactNode } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { CategoryEditModalInnerProps } from '~/components/Board/Sections/Category/CategoryEditModal'; -import { useConfigContext } from '~/config/provider'; -import { useConfigStore } from '~/config/store'; import { openContextModalGeneric } from '~/tools/mantineModalManagerExtensions'; -import { generateDefaultApp } from '~/tools/shared/app'; -import { AppType } from '~/types/app'; +import { generateDefaultApp2 } from '~/tools/shared/app'; +import { RouterOutputs } from '~/utils/api'; +import { EditAppModalInnerProps } from '../../Items/App/EditAppModal'; +import { useCategoryActions } from '../../Sections/Category/Actions/category-actions'; +import { AppItem, CategorySection, EmptySection } from '../../context'; import { useStyles } from '../Shared/styles'; interface AvailableElementTypesProps { modalId: string; + board: RouterOutputs['boards']['byName']; onOpenIntegrations: () => void; - onOpenStaticElements: () => void; } export const AvailableElementTypes = ({ modalId, + board, onOpenIntegrations: onOpenWidgets, - onOpenStaticElements, }: AvailableElementTypesProps) => { const { t } = useTranslation('layout/element-selector/selector'); - const { config, name: configName } = useConfigContext(); - const { updateConfig } = useConfigStore(); - const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0]; + const { addCategory } = useCategoryActions({ boardName: board.name }); + const lastSection = board.sections + .filter((x): x is CategorySection | EmptySection => x.type === 'empty' || x.type === 'category') + .sort((a, b) => b.position - a.position) + .at(0); const onClickCreateCategory = async () => { + if (!lastSection) return; openContextModalGeneric({ modal: 'categoryEditModal', title: t('category.newName'), @@ -43,33 +47,12 @@ export const AvailableElementTypes = ({ position: 0, // doesn't matter, is being overwritten }, onSuccess: async (category) => { - if (!configName) return; - - await updateConfig(configName, (previousConfig) => ({ - ...previousConfig, - wrappers: [ - ...previousConfig.wrappers, - { - id: uuidv4(), - // Thank you ChatGPT ;) - position: previousConfig.categories.length + 1, - }, - ], - categories: [ - ...previousConfig.categories, - { - id: uuidv4(), - name: category.name, - position: previousConfig.categories.length + 1, - }, - ], - })).then(() => { - closeModal(modalId); - showNotification({ - title: t('category.created.title'), - message: t('category.created.message', { name: category.name }), - color: 'teal', - }); + addCategory({ name: category.name, position: lastSection.position + 1 }); + closeModal(modalId); + showNotification({ + title: t('category.created.title'), + message: t('category.created.message', { name: category.name }), + color: 'teal', }); }, }, @@ -85,11 +68,11 @@ export const AvailableElementTypes = ({ name={t('apps')} icon={} onClick={() => { - openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({ + openContextModalGeneric({ modal: 'editApp', innerProps: { - app: generateDefaultApp(getLowestWrapper()?.id ?? 'default'), - // TODO: Add translation? t('app.defaultName') + app: generateDefaultApp2() as AppItem, + board: board, allowAppNamePropagation: true, }, size: 'xl', diff --git a/src/components/Board/SelectElement/SelectElementModal.tsx b/src/components/Board/SelectElement/SelectElementModal.tsx index ca0852663..f69d3cdb8 100644 --- a/src/components/Board/SelectElement/SelectElementModal.tsx +++ b/src/components/Board/SelectElement/SelectElementModal.tsx @@ -1,29 +1,26 @@ import { ContextModalProps } from '@mantine/modals'; import { useState } from 'react'; +import { RouterOutputs } from '~/utils/api'; import { AvailableElementTypes } from './Overview/AvailableElementsOverview'; -import { AvailableStaticTypes } from './StaticElementsTab/AvailableStaticElementsTab'; import { AvailableIntegrationElements } from './WidgetsTab/AvailableWidgetsTab'; -export const SelectElementModal = ({ context, id }: ContextModalProps) => { - const [activeTab, setActiveTab] = useState(); - - switch (activeTab) { - case undefined: - return ( - setActiveTab('integrations')} - onOpenStaticElements={() => setActiveTab('static_elements')} - /> - ); - case 'integrations': - return setActiveTab(undefined)} />; - case 'static_elements': - return setActiveTab(undefined)} />; - default: - /* default to the main selection tab */ - setActiveTab(undefined); - return <>; - } +type InnerProps = { + board: RouterOutputs['boards']['byName']; +}; + +export const SelectElementModal = ({ id, innerProps }: ContextModalProps) => { + const [activeTab, setActiveTab] = useState(); + + if (activeTab === 'integrations') { + return setActiveTab(undefined)} />; + } + + return ( + setActiveTab('integrations')} + /> + ); }; diff --git a/src/components/Board/SelectElement/StaticElementsTab/AvailableStaticElementsTab.tsx b/src/components/Board/SelectElement/StaticElementsTab/AvailableStaticElementsTab.tsx deleted file mode 100644 index a17f0f724..000000000 --- a/src/components/Board/SelectElement/StaticElementsTab/AvailableStaticElementsTab.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Grid, Text } from '@mantine/core'; -import { IconCursorText } from '@tabler/icons-react'; -import { useTranslation } from 'next-i18next'; - -import { GenericAvailableElementType } from '../Shared/GenericElementType'; -import { SelectorBackArrow } from '../Shared/SelectorBackArrow'; - -interface AvailableStaticTypesProps { - onClickBack: () => void; -} - -export const AvailableStaticTypes = ({ onClickBack }: AvailableStaticTypesProps) => { - const { t } = useTranslation('layout/element-selector/selector'); - return ( - <> - - - - Static elements provide you additional control over your dashboard. They are static, because - they don't integrate with any apps and their content never changes. - - - - {}} - /> - - - ); -}; diff --git a/src/components/Board/gridstack/init-gridstack.ts b/src/components/Board/gridstack/init-gridstack.ts index 8dba1646b..692c46f7f 100644 --- a/src/components/Board/gridstack/init-gridstack.ts +++ b/src/components/Board/gridstack/init-gridstack.ts @@ -65,7 +65,7 @@ function setAttributesFromShape(ref: GridItemHTMLElement | null, item: Item) { ref.setAttribute('gs-h', item.height.toString()); } -export type TileWithUnknownLocation = { +export type ItemWithUnknownLocation = { x?: number; y?: number; w?: number; diff --git a/src/components/layout/Templates/BoardLayout.tsx b/src/components/layout/Templates/BoardLayout.tsx index 5e4641d8d..c057a0e83 100644 --- a/src/components/layout/Templates/BoardLayout.tsx +++ b/src/components/layout/Templates/BoardLayout.tsx @@ -189,6 +189,7 @@ const ToggleEditModeButton = () => { const AddElementButton = () => { const { t } = useTranslation('layout/element-selector/selector'); + const board = useRequiredBoard(); return ( @@ -198,7 +199,9 @@ const AddElementButton = () => { modal: 'selectElement', title: t('modal.title'), size: 'xl', - innerProps: {}, + innerProps: { + board, + }, }) } > diff --git a/src/server/db/items.ts b/src/server/db/items.ts index 1a89746a3..ad207429c 100644 --- a/src/server/db/items.ts +++ b/src/server/db/items.ts @@ -48,8 +48,8 @@ export const widgetOptionTypes = [ 'array', 'null', ] as const; -export const appNamePosition = ['right', 'left', 'top', 'bottom'] as const; -export const appNameStyles = ['show', 'hide', 'hover'] as const; +export const appNamePositions = ['right', 'left', 'top', 'bottom'] as const; +export const appNameStyles = ['normal', 'hide', 'hover'] as const; export const statusCodeTypes = [ 'information', 'success', @@ -158,7 +158,7 @@ export type IntegrationSecretVisibility = (typeof integrationSecretVisibility)[n export type IntegrationSecretKey = keyof typeof integrationSecrets; export type WidgetType = (typeof widgetTypes)[number]; export type WidgetOptionType = (typeof widgetOptionTypes)[number]; -export type AppNamePosition = (typeof appNamePosition)[number]; +export type AppNamePosition = (typeof appNamePositions)[number]; export type AppNameStyle = (typeof appNameStyles)[number]; export type StatusCodeType = (typeof statusCodeTypes)[number]; export type SectionType = (typeof sectionTypes)[number]; diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 450c5f3fb..c89a2de7c 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -199,7 +199,7 @@ export const apps = sqliteTable('app', { description: text('description'), internalUrl: text('internal_url').notNull(), externalUrl: text('external_url'), - iconUrl: text('icon_url'), + iconUrl: text('icon_url').notNull(), integrationId: text('integration_id').references(() => integrations.id, { onDelete: 'cascade' }), }); @@ -208,7 +208,7 @@ export const appItems = sqliteTable('app_item', { isPingEnabled: int('is_ping_enabled', { mode: 'boolean' }).notNull().default(false), fontSize: int('font_size').notNull().default(16), namePosition: text('name_position').$type().notNull().default('top'), - nameStyle: text('name_style').$type().notNull().default('show'), + nameStyle: text('name_style').$type().notNull().default('normal'), nameLineClamp: int('name_line_clamp').notNull().default(1), appId: text('app_id') .notNull() diff --git a/src/tools/shared/app.ts b/src/tools/shared/app.ts index c4d869913..40022c8d3 100644 --- a/src/tools/shared/app.ts +++ b/src/tools/shared/app.ts @@ -1,6 +1,26 @@ import { v4 as uuidv4 } from 'uuid'; +import { AppItem } from '~/components/Board/context'; import { AppType } from '~/types/app'; +export const generateDefaultApp2 = (): Omit => ({ + id: uuidv4(), + type: 'app', + name: 'Your app', + internalUrl: 'https://homarr.dev', + externalUrl: 'https://homarr.dev', + iconUrl: '/imgs/logo/logo.png', + nameStyle: 'normal', + namePosition: 'top', + nameLineClamp: 1, + fontSize: 16, + isPingEnabled: true, + statusCodes: [200, 301, 302, 304, 307, 308], + openInNewTab: true, + description: null, + integration: null, + integrationId: null, +}); + export const generateDefaultApp = (wrapperId: string): AppType => ({ id: uuidv4(), @@ -11,7 +31,7 @@ export const generateDefaultApp = (wrapperId: string): AppType => appNameStatus: 'normal', positionAppName: 'column', lineClampAppName: 1, - appNameFontSize: 16 + appNameFontSize: 16, }, network: { enabledStatusChecker: true,