From fd4dbac787e509b0be1467263675b74e04675827 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sat, 21 Oct 2023 11:38:51 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20addition=20of=20widgets=20on?= =?UTF-8?q?=20board?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Board/Items/App/EditAppModal.tsx | 1 + .../Board/Items/App/app-actions.tsx | 3 +- .../Board/Items/Widget/WidgetsEditModal.tsx | 2 +- .../Board/Items/Widget/widget-actions.ts | 105 ++++++++++++++++++ .../SelectElement/SelectElementModal.tsx | 8 +- .../WidgetsTab/AvailableWidgetsTab.tsx | 16 ++- .../WidgetsTab/WidgetElementType.tsx | 98 ++++------------ src/components/Board/context.tsx | 2 - .../Board/gridstack/init-gridstack.ts | 9 -- .../Board/gridstack/use-gridstack.ts | 2 + src/components/Board/widget-actions.ts | 41 ------- src/server/api/routers/board.ts | 36 +----- src/server/db/items.ts | 4 +- src/server/db/schema.ts | 9 +- src/tools/shared/app.ts | 9 +- src/validations/widget.ts | 20 ++++ 16 files changed, 182 insertions(+), 183 deletions(-) create mode 100644 src/components/Board/Items/Widget/widget-actions.ts delete mode 100644 src/components/Board/widget-actions.ts create mode 100644 src/validations/widget.ts diff --git a/src/components/Board/Items/App/EditAppModal.tsx b/src/components/Board/Items/App/EditAppModal.tsx index 329280b2e..6cc073d73 100644 --- a/src/components/Board/Items/App/EditAppModal.tsx +++ b/src/components/Board/Items/App/EditAppModal.tsx @@ -61,6 +61,7 @@ export const EditAppModal = ({ const onSubmit = (values: FormType) => { createOrUpdateApp({ app: values }); + console.log(values); // also close the parent modal context.closeAll(); }; diff --git a/src/components/Board/Items/App/app-actions.tsx b/src/components/Board/Items/App/app-actions.tsx index 176058ce7..001bc0bd3 100644 --- a/src/components/Board/Items/App/app-actions.tsx +++ b/src/components/Board/Items/App/app-actions.tsx @@ -24,7 +24,6 @@ export const useAppActions = ({ boardName }: { boardName: string }) => { sectionId = prev.sections .filter((section): section is EmptySection => section.type === 'empty') .sort((a, b) => a.position - b.position)[0].id; - console.log(sectionId); } return { @@ -32,9 +31,9 @@ export const useAppActions = ({ boardName }: { boardName: string }) => { sections: prev.sections.map((section) => { // Return same section if item is not in it if (section.id !== sectionId) return section; - console.log(section); return { ...section, + // Width, height, x, y are defined by gridstack afterwards items: section.items.filter((item) => item.id !== app.id).concat(app as AppItem), }; }), diff --git a/src/components/Board/Items/Widget/WidgetsEditModal.tsx b/src/components/Board/Items/Widget/WidgetsEditModal.tsx index 3760814e9..f796a4ee0 100644 --- a/src/components/Board/Items/Widget/WidgetsEditModal.tsx +++ b/src/components/Board/Items/Widget/WidgetsEditModal.tsx @@ -18,8 +18,8 @@ import { ContextModalProps } from '@mantine/modals'; import { IconAlertTriangle, IconPlaylistX, IconPlus } from '@tabler/icons-react'; import { Trans, useTranslation } from 'next-i18next'; import { FC, useState } from 'react'; +import { useWidgetActions } from '~/components/Board/Items/Widget/widget-actions'; import { WidgetItem } from '~/components/Board/context'; -import { useWidgetActions } from '~/components/Board/widget-actions'; import { useConfigContext } from '~/config/provider'; import { useConfigStore } from '~/config/store'; import { mapObject } from '~/tools/client/objects'; diff --git a/src/components/Board/Items/Widget/widget-actions.ts b/src/components/Board/Items/Widget/widget-actions.ts new file mode 100644 index 000000000..eae2aa5c0 --- /dev/null +++ b/src/components/Board/Items/Widget/widget-actions.ts @@ -0,0 +1,105 @@ +import { useCallback } from 'react'; +import { v4 } from 'uuid'; +import { z } from 'zod'; +import { api } from '~/utils/api'; +import { widgetCreationSchema, widgetSortSchema } from '~/validations/widget'; +import { IWidget, IWidgetDefinition } from '~/widgets/widgets'; + +import { EmptySection, WidgetItem } from '../../context'; + +type UpdateWidgetOptions = { + itemId: string; + newOptions: Record; +}; + +type CreateWidget = { + sort: z.infer; + definition: IWidgetDefinition; +}; + +export const useWidgetActions = ({ boardName }: { boardName: string }) => { + const utils = api.useContext(); + const updateWidgetOptions = useCallback( + ({ itemId, newOptions }: UpdateWidgetOptions) => { + utils.boards.byName.setData({ boardName }, (prev) => { + if (!prev) return prev; + return { + ...prev, + sections: prev.sections.map((section) => { + // Return same section if item is not in it + if (!section.items.some((item) => item.id === itemId)) return section; + return { + ...section, + items: section.items.map((item) => { + // Return same item if item is not the one we're moving + if (item.id !== itemId || item.type !== 'widget') return item; + return { + ...item, + options: newOptions, + }; + }), + }; + }), + }; + }); + }, + [boardName, utils] + ); + + const createWidget = useCallback( + ({ sort, definition }: CreateWidget) => { + utils.boards.byName.setData({ boardName }, (prev) => { + if (!prev) return prev; + + let lastSection = prev.sections + .filter((s): s is EmptySection => s.type === 'empty') + .sort((a, b) => a.position - b.position)[0]; + + const widget = { + id: v4(), + type: 'widget', + sort, + options: Object.entries(definition.options).reduce( + (prev, [k, v]) => { + const newPrev = prev; + newPrev[k] = v.defaultValue; + return newPrev; + }, + {} as Record + ), + } satisfies z.infer; + + return { + ...prev, + sections: prev.sections.map((section) => { + // Return same section if item is not in it + if (section.id !== lastSection.id) return section; + return { + ...section, + items: section.items.concat(applyMinSize(widget, definition)), + }; + }), + }; + }); + }, + [boardName, utils] + ); + + return { + createWidget, + updateWidgetOptions, + }; +}; + +// TODO: When section size is declared it should be used to calculate the min size +const applyMinSize = ( + widget: z.infer, + definition: IWidgetDefinition +): WidgetItem => { + // Width, height, x, y are defined by gridstack afterwards + return { + ...widget, + width: definition.gridstack.minWidth, + height: definition.gridstack.minHeight, + } as WidgetItem; +}; diff --git a/src/components/Board/SelectElement/SelectElementModal.tsx b/src/components/Board/SelectElement/SelectElementModal.tsx index f69d3cdb8..c9e7f0398 100644 --- a/src/components/Board/SelectElement/SelectElementModal.tsx +++ b/src/components/Board/SelectElement/SelectElementModal.tsx @@ -13,7 +13,13 @@ export const SelectElementModal = ({ id, innerProps }: ContextModalProps(); if (activeTab === 'integrations') { - return setActiveTab(undefined)} />; + return ( + setActiveTab(undefined)} + /> + ); } return ( diff --git a/src/components/Board/SelectElement/WidgetsTab/AvailableWidgetsTab.tsx b/src/components/Board/SelectElement/WidgetsTab/AvailableWidgetsTab.tsx index d1de06477..5b4ee516e 100644 --- a/src/components/Board/SelectElement/WidgetsTab/AvailableWidgetsTab.tsx +++ b/src/components/Board/SelectElement/WidgetsTab/AvailableWidgetsTab.tsx @@ -1,15 +1,20 @@ import { Grid, Text } from '@mantine/core'; import { useTranslation } from 'next-i18next'; +import { objectEntries } from '~/tools/object'; import widgets from '../../../../widgets'; import { SelectorBackArrow } from '../Shared/SelectorBackArrow'; import { WidgetElementType } from './WidgetElementType'; interface AvailableIntegrationElementsProps { + modalId: string; + boardName: string; onClickBack: () => void; } export const AvailableIntegrationElements = ({ + modalId, + boardName, onClickBack, }: AvailableIntegrationElementsProps) => { const { t } = useTranslation('layout/element-selector/selector'); @@ -22,8 +27,15 @@ export const AvailableIntegrationElements = ({ - {Object.entries(widgets).map(([k, v]) => ( - + {objectEntries(widgets).map(([k, v]) => ( + ))} diff --git a/src/components/Board/SelectElement/WidgetsTab/WidgetElementType.tsx b/src/components/Board/SelectElement/WidgetsTab/WidgetElementType.tsx index 8dac04029..125efcdfd 100644 --- a/src/components/Board/SelectElement/WidgetsTab/WidgetElementType.tsx +++ b/src/components/Board/SelectElement/WidgetsTab/WidgetElementType.tsx @@ -2,95 +2,39 @@ import { useModals } from '@mantine/modals'; import { showNotification } from '@mantine/notifications'; import { Icon, IconChecks } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; -import { v4 as uuidv4 } from 'uuid'; -import { useConfigContext } from '~/config/provider'; -import { useConfigStore } from '~/config/store'; -import { IWidget, IWidgetDefinition } from '~/widgets/widgets'; +import { WidgetSort } from '~/server/db/items'; +import { IWidgetDefinition } from '~/widgets/widgets'; -import { useEditModeStore } from '../../useEditModeStore'; +import { useWidgetActions } from '../../Items/Widget/widget-actions'; import { GenericAvailableElementType } from '../Shared/GenericElementType'; interface WidgetElementTypeProps { - id: string; + sort: WidgetSort; + boardName: string; image: string | Icon; disabled?: boolean; widget: IWidgetDefinition; + modalId: string; } -export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElementTypeProps) => { +export const WidgetElementType = ({ + sort, + image, + disabled, + widget, + boardName, + modalId, +}: WidgetElementTypeProps) => { const { closeModal } = useModals(); - 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; - - const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0]; + const { t } = useTranslation(`modules/${sort}`); + const { createWidget } = useWidgetActions({ boardName }); const handleAddition = async () => { - updateConfig( - configName, - (prev) => ({ - ...prev, - widgets: [ - ...prev.widgets, - { - id: uuidv4(), - type: widget.id, - properties: Object.entries(widget.options).reduce( - (prev, [k, v]) => { - const newPrev = prev; - newPrev[k] = v.defaultValue; - return newPrev; - }, - {} as IWidget['properties'] - ), - area: { - type: 'wrapper', - properties: { - id: getLowestWrapper()?.id ?? '', - }, - }, - shape: { - sm: { - location: { - x: 0, - y: 0, - }, - size: { - width: widget.gridstack.minWidth, - height: widget.gridstack.minHeight, - }, - }, - md: { - location: { - x: 0, - y: 0, - }, - size: { - width: widget.gridstack.minWidth, - height: widget.gridstack.minHeight, - }, - }, - lg: { - location: { - x: 0, - y: 0, - }, - size: { - width: widget.gridstack.minWidth, - height: widget.gridstack.minHeight, - }, - }, - }, - }, - ], - }), - true, - !isEditMode - ); - closeModal('selectElement'); + createWidget({ + sort, + definition: widget, + }); + closeModal(modalId); showNotification({ title: t('descriptor.name'), message: t('descriptor.description'), diff --git a/src/components/Board/context.tsx b/src/components/Board/context.tsx index 554347ee5..6a47e99f3 100644 --- a/src/components/Board/context.tsx +++ b/src/components/Board/context.tsx @@ -63,5 +63,3 @@ type ItemOfType = TItem exte : never; export type AppItem = ItemOfType; export type WidgetItem = ItemOfType; - -export type IntegrationSecret = Exclude['secrets'][number]; diff --git a/src/components/Board/gridstack/init-gridstack.ts b/src/components/Board/gridstack/init-gridstack.ts index 692c46f7f..36d21182f 100644 --- a/src/components/Board/gridstack/init-gridstack.ts +++ b/src/components/Board/gridstack/init-gridstack.ts @@ -51,20 +51,11 @@ export const initializeGridstack = ({ grid.removeAll(false); section.items.forEach((item) => { const ref = refs.items.current[item.id]?.current; - setAttributesFromShape(ref, item); ref && grid.makeWidget(ref); }); grid.batchUpdate(false); }; -function setAttributesFromShape(ref: GridItemHTMLElement | null, item: Item) { - if (!item || !ref) return; - ref.setAttribute('gs-x', item.x.toString()); - ref.setAttribute('gs-y', item.y.toString()); - ref.setAttribute('gs-w', item.width.toString()); - ref.setAttribute('gs-h', item.height.toString()); -} - export type ItemWithUnknownLocation = { x?: number; y?: number; diff --git a/src/components/Board/gridstack/use-gridstack.ts b/src/components/Board/gridstack/use-gridstack.ts index 0f6a9278d..c733871bc 100644 --- a/src/components/Board/gridstack/use-gridstack.ts +++ b/src/components/Board/gridstack/use-gridstack.ts @@ -91,7 +91,9 @@ export const useGridstack = ({ section }: UseGridstackProps): UseGristackReturnT const onAdd = (addedNode: GridStackNode) => { const itemType = addedNode.el?.getAttribute('data-type'); const itemId = addedNode.el?.getAttribute('data-id'); + console.log('onAdd', itemType, itemId); if (!itemType || !itemId) return; + console.log('onAdd', addedNode); // Updates the react-query state moveItemToSection({ diff --git a/src/components/Board/widget-actions.ts b/src/components/Board/widget-actions.ts deleted file mode 100644 index 17aba4dd5..000000000 --- a/src/components/Board/widget-actions.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useCallback } from 'react'; -import { api } from '~/utils/api'; - -type UpdateWidgetOptions = { - itemId: string; - newOptions: Record; -}; - -export const useWidgetActions = ({ boardName }: { boardName: string }) => { - const utils = api.useContext(); - const updateWidgetOptions = useCallback( - ({ itemId, newOptions }: UpdateWidgetOptions) => { - utils.boards.byName.setData({ boardName }, (prev) => { - if (!prev) return prev; - return { - ...prev, - sections: prev.sections.map((section) => { - // Return same section if item is not in it - if (!section.items.some((item) => item.id === itemId)) return section; - return { - ...section, - items: section.items.map((item) => { - // Return same item if item is not the one we're moving - if (item.id !== itemId || item.type !== 'widget') return item; - return { - ...item, - options: newOptions, - }; - }), - }; - }), - }; - }); - }, - [boardName, utils] - ); - - return { - updateWidgetOptions, - }; -}; diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index db89d09fe..fb543bd27 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -4,7 +4,7 @@ import { eq, inArray } from 'drizzle-orm'; import fs from 'fs'; import { z } from 'zod'; import { db } from '~/server/db'; -import { LayoutKind, WidgetType } from '~/server/db/items'; +import { LayoutKind, WidgetSort } from '~/server/db/items'; import { getDefaultBoardAsync } from '~/server/db/queries/userSettings'; import { appItems, @@ -396,7 +396,7 @@ const addLayoutAsync = async (props: AddLayoutProps) => { type AddWidgetProps = { boardId: string; sectionId: string; - type: WidgetType; + type: WidgetSort; width: number; height: number; x: number; @@ -483,11 +483,6 @@ const getAppsForSectionsAsync = async (sectionIds: string[]) => { with: { app: { with: { - integration: { - with: { - secrets: true, - }, - }, statusCodes: { columns: { code: true, @@ -561,7 +556,6 @@ type FullBoardWithLayout = Exclude< type MapSection = FullBoardWithLayout['layouts'][number]['sections'][number]; type MapApp = Awaited>[number]; type MapWidget = Awaited>[number]; -type MapSecret = Exclude['secrets'][number]; type MapOption = MapWidget['options'][number]; const mapSection = ( @@ -625,40 +619,16 @@ const mapApp = (appItem: MapApp) => { const { sectionId, itemId, id, ...commonLayoutItem } = appItem.item.layouts.at(0)!; const common = { ...commonLayoutItem, id: itemId }; const { app: innerApp, appId, itemId: _itemId, item, ...otherAppItem } = appItem; - const { id: _id, integration, statusCodes, integrationId, ...app } = appItem.app!; + const { id: _id, statusCodes, ...app } = appItem.app!; return { ...common, ...otherAppItem, ...app, type: 'app' as const, - integration: integration - ? { - ...integration, - secrets: integration.secrets.map(mapSecret), - } - : null, statusCodes: statusCodes.map((x) => x.code), }; }; -const mapSecret = ({ integrationId, ...secret }: MapSecret) => { - const isDefined = secret.value !== null && secret.value !== ''; - if (secret.visibility === 'private') { - return { - ...secret, - visibility: 'private' as const, - isDefined, - value: null, - }; - } - - return { - ...secret, - visibility: 'public' as const, - isDefined, - }; -}; - const mapOptions = (options: MapOption[]) => { const result = {} as Record; const sorted = options.sort((a, b) => a.path.localeCompare(b.path)); diff --git a/src/server/db/items.ts b/src/server/db/items.ts index bf3657aa4..d7432749e 100644 --- a/src/server/db/items.ts +++ b/src/server/db/items.ts @@ -39,7 +39,7 @@ export const integrationSecrets = { icon: IconPassword, }, } satisfies Record; -export const widgetTypes = objectKeys(widgets); +export const widgetSorts = objectKeys(widgets); export const widgetOptionTypes = [ 'string', 'number', @@ -157,7 +157,7 @@ export type FirstDayOfWeek = (typeof firstDaysOfWeek)[number]; export type IntegrationType = keyof typeof integrationTypes; export type IntegrationSecretVisibility = (typeof integrationSecretVisibility)[number]; export type IntegrationSecretKey = keyof typeof integrationSecrets; -export type WidgetType = (typeof widgetTypes)[number]; +export type WidgetSort = (typeof widgetSorts)[number]; export type WidgetOptionType = (typeof widgetOptionTypes)[number]; export type AppNamePosition = (typeof appNamePositions)[number]; export type AppNameStyle = (typeof appNameStyles)[number]; diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index b438b2d89..2e474786a 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -14,7 +14,7 @@ import { SectionType, StatusCodeType, WidgetOptionType, - WidgetType, + WidgetSort, } from './items'; export const users = sqliteTable('user', { @@ -161,7 +161,7 @@ export const integrationSecrets = sqliteTable( export const widgets = sqliteTable('widget', { id: text('id').notNull().primaryKey(), - type: text('type').$type().notNull(), + type: text('type').$type().notNull(), itemId: text('item_id') .notNull() .references(() => items.id, { onDelete: 'cascade' }), @@ -197,7 +197,6 @@ export const apps = sqliteTable('app', { internalUrl: text('internal_url').notNull(), externalUrl: text('external_url'), iconUrl: text('icon_url').notNull(), - integrationId: text('integration_id').references(() => integrations.id, { onDelete: 'cascade' }), }); export const appItems = sqliteTable('app_item', { @@ -344,10 +343,6 @@ export const integrationRelations = relations(integrations, ({ many }) => ({ })); export const appRelations = relations(apps, ({ one, many }) => ({ - integration: one(integrations, { - fields: [apps.integrationId], - references: [integrations.id], - }), statusCodes: many(appStatusCodes), items: many(appItems), })); diff --git a/src/tools/shared/app.ts b/src/tools/shared/app.ts index deae7784c..bc86b6005 100644 --- a/src/tools/shared/app.ts +++ b/src/tools/shared/app.ts @@ -1,8 +1,9 @@ import { v4 as uuidv4 } from 'uuid'; -import { AppItem } from '~/components/Board/context'; +import { z } from 'zod'; +import { appFormSchema } from '~/components/Board/Items/App/EditAppModal'; import { AppType } from '~/types/app'; -export const generateDefaultApp2 = (): AppItem => { +export const generateDefaultApp2 = (): z.infer => { const appId = uuidv4(); return { id: appId, @@ -19,10 +20,6 @@ export const generateDefaultApp2 = (): AppItem => { statusCodes: [200, 301, 302, 304, 307, 308], openInNewTab: true, description: null, - height: 1, - width: 1, - x: 0, - y: 0, }; }; diff --git a/src/validations/widget.ts b/src/validations/widget.ts new file mode 100644 index 000000000..0809f8109 --- /dev/null +++ b/src/validations/widget.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; +import { widgetSorts } from '~/server/db/items'; + +export const widgetSortSchema = z.enum([widgetSorts[0], ...widgetSorts.slice(1)]); + +export const widgetCreationSchema = z.object({ + id: z.string(), + type: z.literal('widget'), + sort: widgetSortSchema, + options: z.record(z.string(), z.unknown()), +}); + +export const widgetSchema = z + .object({ + x: z.number().min(0), + y: z.number().min(0), + width: z.number().min(0), + height: z.number().min(0), + }) + .merge(widgetCreationSchema);