From 1b4acb94bbb3b0e7ad4b62bb4d87a61320290994 Mon Sep 17 00:00:00 2001 From: Samed <sbalcioglu@graphpolaris.com> Date: Thu, 26 Sep 2024 05:51:03 +0200 Subject: [PATCH] feat: insight sharing implementation --- libs/shared/lib/components/buttons/Button.tsx | 6 +- .../components/buttons/buttons.module.scss | 6 + .../buttons/buttons.module.scss.d.ts | 1 + .../lib/components/textEditor/TextEditor.tsx | 133 +++++++++++++-- .../textEditor/plugins/ToolbarPlugin.tsx | 8 +- .../data-access/store/alertTemplateSlice.ts | 38 +++++ libs/shared/lib/data-access/store/hooks.ts | 4 + .../data-access/store/reportTemplateSlice.ts | 60 +++++++ libs/shared/lib/data-access/store/store.ts | 4 + .../lib/insight-sharing/InsightDialog.tsx | 45 ++++- .../lib/insight-sharing/SettingsPanel.tsx | 115 +++++++++---- .../insight-sharing/alerting/AlertingForm.tsx | 40 ++++- .../insight-sharing/components/AddItem.tsx | 25 ++- .../components/ReportTemplateForm.tsx | 75 -------- .../insight-sharing/components/Sidebar.tsx | 27 +-- .../reporting/ReportingForm.tsx | 161 +++++++++++++----- 16 files changed, 545 insertions(+), 203 deletions(-) create mode 100644 libs/shared/lib/data-access/store/alertTemplateSlice.ts create mode 100644 libs/shared/lib/data-access/store/reportTemplateSlice.ts delete mode 100644 libs/shared/lib/insight-sharing/components/ReportTemplateForm.tsx diff --git a/libs/shared/lib/components/buttons/Button.tsx b/libs/shared/lib/components/buttons/Button.tsx index 79422a12d..bd697b9f7 100644 --- a/libs/shared/lib/components/buttons/Button.tsx +++ b/libs/shared/lib/components/buttons/Button.tsx @@ -21,6 +21,7 @@ type ButtonProps = { className?: string; style?: React.CSSProperties; tooltip?: string; + active?: boolean; onMouseUp?: (e: any) => void; onMouseDown?: (e: any) => void; onMouseEnter?: (e: any) => void; @@ -54,6 +55,7 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H className, children, tooltip, + active, ...props }, forwardedRef, @@ -139,6 +141,8 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H [iconComponent, label, children], ); + const activeClass = useMemo(() => (active ? styles['btn-active'] : ''), [active, styles]); + const ButtonComponent = as; const isAnchor = as === 'a'; @@ -148,7 +152,7 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H {tooltip && <TooltipContent>{tooltip}</TooltipContent>} <TooltipTrigger> <ButtonComponent - className={`${styles.btn} ${typeClass} ${variantClass} ${sizeClass} ${blockClass} ${roundedClass} ${iconOnlyClass} ${className ? className : ''}`} + className={`${styles.btn} ${typeClass} ${variantClass} ${sizeClass} ${blockClass} ${roundedClass} ${iconOnlyClass} ${activeClass} ${className ? className : ''}`} onClick={onClick} disabled={disabled} aria-label={ariaLabel} diff --git a/libs/shared/lib/components/buttons/buttons.module.scss b/libs/shared/lib/components/buttons/buttons.module.scss index a348a6e10..54af8185c 100644 --- a/libs/shared/lib/components/buttons/buttons.module.scss +++ b/libs/shared/lib/components/buttons/buttons.module.scss @@ -144,3 +144,9 @@ .btn-rounded { @apply rounded-full; } + +.btn-active { + background-color: #e0e0e0; + border: 1px solid #b0b0b0; + color: #000; +} \ No newline at end of file diff --git a/libs/shared/lib/components/buttons/buttons.module.scss.d.ts b/libs/shared/lib/components/buttons/buttons.module.scss.d.ts index ac3cb0637..927d89a44 100644 --- a/libs/shared/lib/components/buttons/buttons.module.scss.d.ts +++ b/libs/shared/lib/components/buttons/buttons.module.scss.d.ts @@ -16,5 +16,6 @@ declare const classNames: { readonly 'btn-ghost': 'btn-ghost'; readonly 'btn-block': 'btn-block'; readonly 'btn-rounded': 'btn-rounded'; + readonly 'btn-active': 'btn-active'; }; export = classNames; diff --git a/libs/shared/lib/components/textEditor/TextEditor.tsx b/libs/shared/lib/components/textEditor/TextEditor.tsx index 8a95a8042..af1957d07 100644 --- a/libs/shared/lib/components/textEditor/TextEditor.tsx +++ b/libs/shared/lib/components/textEditor/TextEditor.tsx @@ -1,43 +1,148 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; -import { EditorState } from 'lexical'; -import { MyOnChangePlugin } from './plugins/StateChangePlugin'; -import { ToolbarPlugin } from './plugins/ToolbarPlugin'; +import { $getRoot, $getSelection, $isRangeSelection, TextFormatType } from 'lexical'; +import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html'; import { ErrorHandler } from './ErrorHandler'; import { Placeholder } from './Placeholder'; +import { Button } from '../buttons/Button'; type TextEditorProps = { - editorState: EditorState | undefined; - setEditorState: (value: EditorState) => void; + initialContent?: string; + onContentChange?: (content: string) => void; showToolbar: boolean; placeholder?: string; }; -export function TextEditor({ editorState, setEditorState, showToolbar, placeholder }: TextEditorProps) { - function onChange(editorState: EditorState) { - setEditorState(editorState); - } +function ToolbarPlugin() { + const [editor] = useLexicalComposerContext(); + const [activeStyles, setActiveStyles] = useState<Set<string>>(new Set()); + + React.useEffect(() => { + return editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const newActiveStyles = new Set<string>(); + if (selection.hasFormat('bold')) newActiveStyles.add('bold'); + if (selection.hasFormat('italic')) newActiveStyles.add('italic'); + if (selection.hasFormat('underline')) newActiveStyles.add('underline'); + setActiveStyles(newActiveStyles); + } + }); + }); + }, [editor]); + + const toggleStyle = (style: TextFormatType) => { + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + selection.formatText(style); + } + }); + }; + + return ( + <div className="flex bg-secondary-50 rounded mt-4 p-2 space-x-2"> + <Button + variantType="secondary" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-format-bold]" + onClick={() => toggleStyle('bold')} + active={activeStyles.has('bold')} + /> + <Button + variantType="secondary" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-format-italic]" + onClick={() => toggleStyle('italic')} + active={activeStyles.has('italic')} + /> + <Button + variantType="secondary" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-format-underlined]" + onClick={() => toggleStyle('underline')} + active={activeStyles.has('underline')} + /> + </div> + ); +} + +function ContentSetterPlugin({ initialContent }: { initialContent: string }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (initialContent) { + editor.update(() => { + const parser = new DOMParser(); + const dom = parser.parseFromString(initialContent, 'text/html'); + const nodes = $generateNodesFromDOM(editor, dom); + const root = $getRoot(); + root.clear(); + root.append(...nodes); + }); + } + }, [editor, initialContent]); + + return null; +} + +function OnChangePlugin({ onContentChange }: { onContentChange?: (content: string) => void }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (!onContentChange) return; + + return editor.registerUpdateListener(() => { + editor.getEditorState().read(() => { + const htmlString = $generateHtmlFromNodes(editor, null); + onContentChange(htmlString); + }); + }); + }, [editor, onContentChange]); + + return null; +} + +export function TextEditor({ + initialContent = '', + onContentChange, + showToolbar, + placeholder, +}: TextEditorProps) { + const theme = { + text: { + bold: 'font-bold', + italic: 'italic', + underline: 'underline', + }, + }; const initialConfig = { namespace: 'MyEditor', - editorState: editorState, + theme, onError: ErrorHandler, }; return ( <LexicalComposer initialConfig={initialConfig}> {showToolbar && <ToolbarPlugin />} - <div className="relative"> + <div className="relative border rounded"> <RichTextPlugin - contentEditable={<ContentEditable className="border min-h-24" />} + contentEditable={<ContentEditable className="min-h-24 p-2" />} placeholder={<Placeholder placeholder={placeholder} />} ErrorBoundary={LexicalErrorBoundary} /> </div> - <MyOnChangePlugin onChange={onChange} /> + <ContentSetterPlugin initialContent={initialContent} /> + <OnChangePlugin onContentChange={onContentChange} /> </LexicalComposer> ); } diff --git a/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx index 93c56d75c..65a9d02ae 100644 --- a/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx +++ b/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx @@ -22,13 +22,7 @@ export function ToolbarPlugin() { <div className="flex bg-secondary-50 rounded mt-4 p-2 space-x-2"> <Button variantType="secondary" variant="ghost" size="xs" iconComponent="icon-[ic--baseline-format-bold]" onClick={formatBold} /> <Button variantType="secondary" variant="ghost" size="xs" iconComponent="icon-[ic--baseline-format-italic]" onClick={formatItalic} /> - <Button - variantType="secondary" - variant="ghost" - size="xs" - iconComponent="icon-[ic--baseline-format-underlined]" - onClick={formatItalic} - /> + <Button variantType="secondary" variant="ghost" size="xs" iconComponent="icon-[ic--baseline-format-underlined]"onClick={formatUnderline}/> </div> ); } diff --git a/libs/shared/lib/data-access/store/alertTemplateSlice.ts b/libs/shared/lib/data-access/store/alertTemplateSlice.ts new file mode 100644 index 000000000..f49d4d64c --- /dev/null +++ b/libs/shared/lib/data-access/store/alertTemplateSlice.ts @@ -0,0 +1,38 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from './store'; + +export interface Alert { + id: string; + name: string; + recipients: string[]; + template: string; + frequency: string; +} + +export interface AlertsState { + alerts: Alert[]; +} + +const initialAlertsState: AlertsState = { + alerts: [], +}; + +const alertsSlice = createSlice({ + name: 'alerts', + initialState: initialAlertsState, + reducers: { + setAlerts: (state: AlertsState, action: PayloadAction<Alert[]>) => { + state.alerts = action.payload; + }, + addAlert: (state: AlertsState, action: PayloadAction<Alert>) => { + state.alerts.push(action.payload); + }, + removeAlert: (state: AlertsState, action: PayloadAction<string>) => { + state.alerts = state.alerts.filter(alert => alert.name !== action.payload); + }, + }, +}); + +export const { setAlerts, addAlert, removeAlert } = alertsSlice.actions; +export const selectAlerts = (state: RootState) => state.alerts.alerts; +export default alertsSlice.reducer; diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts index 9680186ed..55917d5d1 100644 --- a/libs/shared/lib/data-access/store/hooks.ts +++ b/libs/shared/lib/data-access/store/hooks.ts @@ -40,6 +40,7 @@ import { SelectionStateI, FocusStateI, focusState, selectionState } from './inte import { VisualizationSettingsType } from '../../vis/common'; import { PolicyUsersState, selectPolicyState } from './authorizationUsersSlice'; import { PolicyResourcesState, selectResourcesPolicyState } from './authorizationResourcesSlice'; +import { ReportTemplate } from './reportTemplateSlice'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch; @@ -93,3 +94,6 @@ export const useUsersPolicy: () => PolicyUsersState = () => useAppSelector(selec // Authorization Resources Slice export const useResourcesPolicy: () => PolicyResourcesState = () => useAppSelector(selectResourcesPolicyState); + +// Reporting Template Slice +export const useReportTemplates = (): ReportTemplate[] => useAppSelector((state: RootState) => state.reportTemplates.templates); \ No newline at end of file diff --git a/libs/shared/lib/data-access/store/reportTemplateSlice.ts b/libs/shared/lib/data-access/store/reportTemplateSlice.ts new file mode 100644 index 000000000..d6b40db2f --- /dev/null +++ b/libs/shared/lib/data-access/store/reportTemplateSlice.ts @@ -0,0 +1,60 @@ +import { createSlice, PayloadAction} from '@reduxjs/toolkit'; +import type { RootState } from './store'; + +export interface ReportTemplate { + id: string; + name: string; + recipients: string[]; + frequency: string | number; + template: string; +} + +export interface ReportTemplatesState { + templates: ReportTemplate[]; +} + + +const initialTemplates: ReportTemplate[] = [ + { + id: 'template1', + name: 'Weekly Report', + recipients: ['test1@graphpolaris.com'], + frequency: 'Weekly', + template: '<p>This is a weekly report template.</p>', + }, + { + id: 'template2', + name: 'Daily Report', + recipients: ['test2@graphpolaris.com'], + frequency: 'Daily', + template: '<p>This is a daily report template.</p>', + }, +]; + +const initialState: ReportTemplatesState = { + templates: initialTemplates, +}; + + +export const reportTemplatesSlice = createSlice({ + name: 'reportTemplates', + initialState, + reducers: { + addTemplate: (state, action: PayloadAction<ReportTemplate>) => { + state.templates.push(action.payload); + }, + updateTemplate: (state, action: PayloadAction<ReportTemplate>) => { + const index = state.templates.findIndex((template) => template.id === action.payload.id); + if (index !== -1) { + state.templates[index] = action.payload; + } + }, + deleteTemplate: (state, action: PayloadAction<string>) => { + state.templates = state.templates.filter((template) => template.id !== action.payload); + }, + }, +}); + +export const { addTemplate, updateTemplate, deleteTemplate } = reportTemplatesSlice.actions; +export const selectReportTemplates = (state: RootState) => state.reportTemplates.templates; +export default reportTemplatesSlice.reducer; \ No newline at end of file diff --git a/libs/shared/lib/data-access/store/store.ts b/libs/shared/lib/data-access/store/store.ts index c1aea4606..e881bbd78 100644 --- a/libs/shared/lib/data-access/store/store.ts +++ b/libs/shared/lib/data-access/store/store.ts @@ -11,6 +11,8 @@ import visualizationSlice from './visualizationSlice'; import interactionSlice from './interactionSlice'; import policyUsersSlice from './authorizationUsersSlice'; import policyPermissionSlice from './authorizationResourcesSlice'; +import reportTemplateSlice from './reportTemplateSlice'; +import alertTemplateSlice from './alertTemplateSlice'; export const store = configureStore({ reducer: { @@ -26,6 +28,8 @@ export const store = configureStore({ visualize: visualizationSlice, policyUsers: policyUsersSlice, policyResources: policyPermissionSlice, + reportTemplates: reportTemplateSlice, + alerts: alertTemplateSlice, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/libs/shared/lib/insight-sharing/InsightDialog.tsx b/libs/shared/lib/insight-sharing/InsightDialog.tsx index 0af6191f2..26ca601bf 100644 --- a/libs/shared/lib/insight-sharing/InsightDialog.tsx +++ b/libs/shared/lib/insight-sharing/InsightDialog.tsx @@ -1,24 +1,54 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect} from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import { Dialog, DialogContent } from '../components'; import { SettingsPanel } from './SettingsPanel'; import { MonitorType, Sidebar } from './components/Sidebar'; +import { RootState } from '../data-access'; +import { addTemplate, ReportTemplate, updateTemplate} from '../data-access/store/reportTemplateSlice'; +import { v4 as uuidv4 } from 'uuid'; +import { useReportTemplates } from '../data-access'; +import { useAppDispatch } from '../data-access'; +import { Alert } from '../data-access/store/alertTemplateSlice'; type Props = { open: boolean; onClose: () => void; }; -const reports = ['Sequence 1', 'Sequence 2']; -const alerts = ['Potential New Incident', 'Potential New Info']; export function InsightDialog(props: Props) { + const dispatch = useAppDispatch(); + const reportTemplates = useReportTemplates(); + const alerts: Alert[] = useSelector((state: RootState) => state.alerts.alerts || []); const [adding, setAdding] = useState<boolean>(false); const [active, setActive] = useState<string>(''); const [activeCategory, setActiveCategory] = useState<MonitorType | undefined>(undefined); - const handleChangeActive = (category: MonitorType, name: string) => { - setActive(name); + const handleChangeActive = (category: MonitorType, id: string) => { + setActive(id); setActiveCategory(category); + setAdding(false) + }; + + const handleSaveTemplate = (templateData: Omit<ReportTemplate, 'id'>) => { + const existingTemplate = reportTemplates.find((t) => t.id === active); + + if (existingTemplate) { + const updatedTemplate: ReportTemplate = { + ...existingTemplate, + ...templateData, + }; + dispatch(updateTemplate(updatedTemplate)); + setActive(updatedTemplate.id); + } else { + const newTemplate: ReportTemplate = { + ...templateData, + id: uuidv4(), + }; + dispatch(addTemplate(newTemplate)); + setActive(newTemplate.id); + } + setAdding(false); }; return ( @@ -31,7 +61,7 @@ export function InsightDialog(props: Props) { <DialogContent className="w-5/6 h-5/6 rounded-none py-0 px-0"> <div className="flex w-full h-full"> <Sidebar - reports={reports} + reports={reportTemplates} alerts={alerts} changeActive={handleChangeActive} setAdding={setAdding} @@ -43,6 +73,9 @@ export function InsightDialog(props: Props) { adding={adding} setAdding={setAdding} setActiveCategory={setActiveCategory} + setActive={setActive} + onSaveTemplate={handleSaveTemplate} + templates={reportTemplates} /> </div> </DialogContent> diff --git a/libs/shared/lib/insight-sharing/SettingsPanel.tsx b/libs/shared/lib/insight-sharing/SettingsPanel.tsx index 9d0a7a676..84a51b0d1 100644 --- a/libs/shared/lib/insight-sharing/SettingsPanel.tsx +++ b/libs/shared/lib/insight-sharing/SettingsPanel.tsx @@ -1,10 +1,11 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { ReportingForm } from './reporting/ReportingForm'; import { AlertingForm } from './alerting/AlertingForm'; -import { Button, Icon } from '../components'; import { AddItem } from './components/AddItem'; import { StartScreen } from './components/StartScreen'; import { MonitorType } from './components/Sidebar'; +import { ReportTemplate } from '../data-access/store/reportTemplateSlice'; +import { v4 as uuidv4 } from 'uuid'; type Props = { active: string; @@ -12,42 +13,92 @@ type Props = { adding: boolean; setAdding: (val: boolean) => void; setActiveCategory: (val: MonitorType | undefined) => void; + setActive: (val: string) => void; + onSaveTemplate: (templateData: ReportTemplate) => void; + templates?: ReportTemplate[]; }; export function SettingsPanel(props: Props) { - const [contacts, setContacts] = useState<string[]>([]); + const { + active, + activeCategory, + adding, + setAdding, + setActiveCategory, + setActive, + onSaveTemplate, + templates = [], + } = props; - const getContacts = (): string[] => ['Jan', 'Piet']; + const activeTemplate = templates.find((template) => template.id === active); - useEffect(() => { - if (!contacts) { - const userContacts = getContacts(); - setContacts(userContacts); + const handleAddItem = (name: string) => { + if (activeCategory === 'report') { + const newTemplate: ReportTemplate = { + id: uuidv4(), + name, + recipients: [], + frequency: 'Daily', + template: '', + }; + onSaveTemplate(newTemplate); + setAdding(false); + setActive(newTemplate.id); + setActiveCategory('report') } - }, []); - - return props.activeCategory ? ( - <div className="w-3/4 p-4"> - {props.adding ? ( - <AddItem category={props.activeCategory} /> - ) : ( - props.active && - props.activeCategory && ( - <div> - {props.activeCategory === 'report' ? ( - <ReportingForm activeTemplate={props.active} /> - ) : ( - <AlertingForm activeTemplate={props.active} /> - )} - </div> - ) - )} - <div className="flex justify-end mt-2"> - <Button label="Delete" variantType="secondary" variant="outline" /> - <Button label="Save" variantType="primary" className="ml-2" /> - </div> + }; + + const handleSaveTemplate = (templateData: Omit<ReportTemplate, 'id'>) => { + const fullTemplateData: ReportTemplate = { + ...templateData, + id: activeTemplate?.id || '', + }; + onSaveTemplate(fullTemplateData); + }; + + const renderContent = () => { + if (adding) { + return ( + <AddItem + category={activeCategory as MonitorType} + onAdd={handleAddItem} + /> + ); + } + + if (!activeCategory || !active) { + return ( + <StartScreen + setAdding={setAdding} + setActiveCategory={setActiveCategory} + /> + ); + } + + switch (activeCategory) { + case 'report': + return ( + <ReportingForm + onSave={onSaveTemplate} + initialData={activeTemplate} + /> + ); + //case 'alert': + // return ( + // <AlertingForm + // initialData={activeAlert} + ///> + // ); + default: + return null; + } + }; + + console.log('Templates in SettingsPanel:', templates); + + return ( + <div className="flex-1 p-4 overflow-auto"> + {renderContent()} </div> - ) : ( - <StartScreen setAdding={props.setAdding} setActiveCategory={props.setActiveCategory} /> ); } diff --git a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx b/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx index 11ea8d907..f048be166 100644 --- a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx +++ b/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx @@ -1,18 +1,33 @@ import React, { useState } from 'react'; import { Accordion, AccordionItem, AccordionHead, AccordionBody } from '../../components/accordion'; import { TextEditor } from '../../components/textEditor'; -import { EditorState } from 'lexical'; +import { EditorState , createEditor} from 'lexical'; +import { Alert } from '../../data-access/store/alertTemplateSlice'; type Props = { - activeTemplate: string; + initialData?: Alert; + onSave: (alertData: Alert) => void; }; -export function AlertingForm(props: Props) { - const [editorState, setEditorState] = useState<EditorState | undefined>(undefined); +export function AlertingForm({ initialData, onSave }: Props) { + const [editorState, setEditorState] = useState<EditorState | undefined>( + initialData?.template ? createEditor().parseEditorState(initialData.template) : undefined + ); + const [recipients, setRecipients] = useState<string>(initialData?.recipients.join(', ') || ''); + + const handleSave = () => { + const updatedAlert: Alert = { + ...initialData, + recipients: recipients.split(',').map(r => r.trim()), + template: JSON.stringify(editorState?.toJSON() ?? ''), + frequency: initialData?.frequency ?? 'Immediate', + }; + onSave(updatedAlert); + }; return ( <div> - <span className="text-lg text-secondary-600 font-bold mb-4">Alert ID: {props.activeTemplate}</span> + <span className="text-lg text-secondary-600 font-bold mb-4">Alert ID: {initialData?.id || 'New Alert'}</span> <Accordion defaultOpenAll={true} className="border-t divide-y"> <AccordionItem className="pt-2 pb-4"> <AccordionHead showArrow={false}> @@ -20,7 +35,12 @@ export function AlertingForm(props: Props) { </AccordionHead> <AccordionBody> <div> - <input className="border" /> + <input + className="border w-full p-2" + value={recipients} + onChange={(e) => setRecipients(e.target.value)} + placeholder="Enter recipients, separated by commas" + /> </div> </AccordionBody> </AccordionItem> @@ -38,6 +58,12 @@ export function AlertingForm(props: Props) { </AccordionBody> </AccordionItem> </Accordion> + <button + className="mt-4 px-4 py-2 bg-blue-500 text-white rounded" + onClick={handleSave} + > + Save Alert + </button> </div> ); -} +} \ No newline at end of file diff --git a/libs/shared/lib/insight-sharing/components/AddItem.tsx b/libs/shared/lib/insight-sharing/components/AddItem.tsx index 771ab6bcc..5ec415d0b 100644 --- a/libs/shared/lib/insight-sharing/components/AddItem.tsx +++ b/libs/shared/lib/insight-sharing/components/AddItem.tsx @@ -1,18 +1,35 @@ import React, { useState } from 'react'; -import { Input } from '../../components'; +import { Input, Button } from '../../components'; import { MonitorType } from './Sidebar'; type Props = { - category: MonitorType; + category: MonitorType; + onAdd: (name: string) => void; }; export function AddItem(props: Props) { - const [value, setValue] = useState<string>(''); + const [name, setName] = useState<string>(''); + + const handleAdd = () => { + if (name.trim()) { + props.onAdd(name.trim()); + setName(''); + } + }; return ( <div> <span className="text-lg text-secondary-600 font-bold mb-4">Add a new {props.category}ing service</span> - <Input type="text" label="Name" value={value} onChange={setValue} /> + <div className="mt-4 space-y-4"> + <Input + type="text" + label="Name" + value={name} + onChange={(value: string) => setName(value)} + placeholder={`Enter ${props.category} name`} + /> + <Button onClick={handleAdd}>Add {props.category}</Button> + </div> </div> ); } diff --git a/libs/shared/lib/insight-sharing/components/ReportTemplateForm.tsx b/libs/shared/lib/insight-sharing/components/ReportTemplateForm.tsx deleted file mode 100644 index 338e5e7ab..000000000 --- a/libs/shared/lib/insight-sharing/components/ReportTemplateForm.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useState, ChangeEvent } from 'react'; -import { Input } from '../../components/inputs'; -import { Button } from '../../components'; -import { TextEditor } from '../../components/textEditor'; -import { EditorState } from 'lexical'; - - -interface TemplateData { - name: string; - recipients: string[]; - frequency: string; - template: string; -} - -interface ReportTemplateFormProps { - onSave: (data: TemplateData) => void; -} - -const ReportTemplateForm: React.FC<ReportTemplateFormProps> = ({ onSave }) => { - const [name, setName] = useState(''); - const [recipients, setRecipients] = useState(''); - const [frequency, setFrequency] = useState<string | number>('Daily'); - const [editorState, setEditorState] = useState<EditorState | undefined>(undefined); - - const handleSave = () => { - if (editorState) { - onSave({ - name, - recipients: recipients.split(',').map(email => email.trim()), - frequency: frequency.toString(), - template: JSON.stringify(editorState.toJSON()) - }); - } - }; - - return ( - <div className="space-y-4"> - <Input - type ="text" - label="Template Name" - value={name} - onChange={(value: string) => setName(value)} - placeholder="Enter template name" - /> - <Input - type = "text" - label="Recipients" - value={recipients} - onChange={(value: string) => setRecipients(value)} - placeholder="Enter email addresses, separated by commas" - /> - <Input - type="dropdown" - label="Frequency" - value={frequency} - onChange={(value: string | number) => setFrequency(value)} - options={['Daily', 'Weekly', 'Monthly']} - /> - <div> - <label className="block text-sm font-medium text-gray-700 mb-1"> - Email Template - </label> - <TextEditor - editorState={editorState} - setEditorState={setEditorState} - showToolbar={true} - placeholder="Start typing your report template..." - /> - </div> - <Button onClick={handleSave}>Save Template</Button> - </div> - ); -}; - -export default ReportTemplateForm; \ No newline at end of file diff --git a/libs/shared/lib/insight-sharing/components/Sidebar.tsx b/libs/shared/lib/insight-sharing/components/Sidebar.tsx index d8ebcfe1c..6a40b3b8c 100644 --- a/libs/shared/lib/insight-sharing/components/Sidebar.tsx +++ b/libs/shared/lib/insight-sharing/components/Sidebar.tsx @@ -1,18 +1,25 @@ import React from 'react'; import { Button } from '../../components'; import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../../components/accordion'; +import { ReportTemplate } from '../../data-access/store/reportTemplateSlice'; +import { Alert } from '../../data-access/store/alertTemplateSlice'; + export type MonitorType = 'report' | 'alert'; type SidebarProps = { - reports: string[]; - alerts: string[]; + reports: ReportTemplate[]; + alerts: Alert[]; changeActive: (category: MonitorType, val: string) => void; setAdding: (val: boolean) => void; setActiveCategory: (val: MonitorType | undefined) => void; }; export function Sidebar(props: SidebarProps) { + const reports = props.reports || []; + const alerts = props.alerts || []; + console.log('Reports in Sidebar:', props.reports); + return ( <div className="w-1/4 border-r overflow-auto flex flex-col h-full"> <span className="text-lg text-secondary-700 font-semibold px-2 py-4">Insight Sharing</span> @@ -41,16 +48,16 @@ export function Sidebar(props: SidebarProps) { </AccordionHead> <AccordionBody className="ml-0"> <ul className="space-y-2"> - {props.reports.map((name, index) => ( + {props.reports.map((template) => ( <li - key={index} + key={template.id} className="cursor-pointer p-2 hover:bg-secondary-50" onClick={() => { - props.changeActive('report', name); + props.changeActive('report', template.id); props.setAdding(false); }} > - {name} + {template.name} </li> ))} </ul> @@ -80,16 +87,16 @@ export function Sidebar(props: SidebarProps) { </AccordionHead> <AccordionBody className="ml-0"> <ul className="space-y-2"> - {props.alerts.map((name, index) => ( + {props.alerts.map((alert) => ( <li - key={index} + key={alert.id} className="cursor-pointer p-2 hover:bg-secondary-50" onClick={() => { - props.changeActive('alert', name); + props.changeActive('alert', alert.id); props.setAdding(false); }} > - {name} + {alert.name} </li> ))} </ul> diff --git a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx index 9b2645b91..1c4bf5326 100644 --- a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx +++ b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx @@ -1,57 +1,124 @@ -import React, { useState } from 'react'; -import { Input, LoadingSpinner } from '../../components'; +import React, { useState, useEffect} from 'react'; +import { Input, Button, LoadingSpinner, DropdownInput } from '../../components'; import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../../components/accordion'; import { TextEditor } from '../../components/textEditor'; import { EditorState } from 'lexical'; +import { ReportTemplate } from '../../data-access/store/reportTemplateSlice'; +import { useAppDispatch } from '../../data-access'; +import { addWarning } from '../../data-access/store/configSlice'; +import { DropdownTrigger, DropdownContainer, DropdownItem, DropdownItemContainer } from '../../components/dropdowns'; +import { v4 as uuidv4 } from 'uuid'; +import { Icon } from '../../components'; -type Props = { - activeTemplate: string; -}; +interface ReportingFormProps { + onSave: (data:ReportTemplate) => void; + initialData?: ReportTemplate; + activeTemplate?: string; +} -export function ReportingForm(props: Props) { +export function ReportingForm({ onSave, initialData, activeTemplate }: ReportingFormProps) { const [loading, setLoading] = useState(false); - const [editorState, setEditorState] = useState<EditorState | undefined>(undefined); + const [name, setName] = useState(initialData?.name || ''); + const [recipients, setRecipients] = useState(initialData?.recipients.join(', ') || ''); + const [frequency, setFrequency] = useState<string>('Daily'); + const [content, setContent] = useState(initialData?.template || ''); + const [isCollapsed, setIsCollapsed] = useState(false); + + const dispatch = useAppDispatch(); + + useEffect(() => { + if (initialData) { + setName(initialData.name || ''); + setRecipients(initialData.recipients.join(', ') || ''); + setFrequency(initialData.frequency.toString() || 'Daily'); + } else { + setName(''); + setRecipients(''); + setFrequency('Daily'); + } + }, [initialData]); - return loading ? ( - <LoadingSpinner /> - ) : ( - props.activeTemplate && ( - <div> - <span className="text-lg text-secondary-600 font-bold mb-4">Report ID: {props.activeTemplate}</span> - <Accordion defaultOpenAll={true} className="border-t divide-y"> - <AccordionItem className="pt-2 pb-4"> - <AccordionHead showArrow={false}> - <span className="font-semibold">Recipient(s)</span> - </AccordionHead> - <AccordionBody> + const handleSave= () => { + if (!content || content.trim() === '') { + dispatch(addWarning('Template content cannot be empty.')); + return; + } + + if (!recipients.trim()) { + dispatch(addWarning('Please add at least one recipient.')); + return; + } + + const templateData: ReportTemplate = { + id: initialData?.id || uuidv4(), + name, + recipients: recipients.split(',').map((email) => email.trim()), + frequency, + template: content, + }; + setLoading(true); + onSave(templateData); + setTimeout(() => setLoading(false), 1000); + }; + + if (loading) return <LoadingSpinner />; + + return ( + <div> + <span className="text-lg text-secondary-600 font-bold mb-4"> + {activeTemplate ? `Report ID: ${activeTemplate}` : 'New Report Template'} + </span> + <Accordion defaultOpenAll={true} className="border-t divide-y"> + <AccordionItem className="pt-4 pb-4"> + <AccordionHead showArrow={true}> + <span className="font-semibold">Report Template</span> + </AccordionHead> + <AccordionBody> + <div className="space-y-4"> + <Input + type="text" + label="Template Name" + value={name} + onChange={(value: string) => setName(value)} + placeholder="Enter template name" + /> + <Input + type="text" + label="Recipients" + value={recipients} + onChange={(value: string) => setRecipients(value)} + placeholder="Enter email addresses" + /> + <div className="form-control w-full"> + <label className="label p-0"> + <span className="text-sm font-medium text-secondary-700">Frequency</span> + </label> + <select + className="select select-bordered w-full focus:outline-none focus:ring-0" + value={frequency} + onChange={(e) => setFrequency(e.target.value)} + > + <option value="Daily">Daily</option> + <option value="Weekly">Weekly</option> + <option value="Monthly">Monthly</option> + </select> + </div> <div> - <input className="border" /> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Email Template + </label> + <TextEditor + initialContent={initialData?.template || ''} + onContentChange={setContent} + showToolbar={true} + placeholder="Start typing your report template..." + /> </div> - </AccordionBody> - </AccordionItem> - <AccordionItem className="pt-2 pb-4"> - <AccordionHead showArrow={false}> - <span className="font-semibold">Repeat</span> - </AccordionHead> - <AccordionBody> - <Input label="Frequency" type="dropdown" value={'Daily'} onChange={() => {}} options={['Daily', 'Weekly']} className="mb-1" /> - </AccordionBody> - </AccordionItem> - <AccordionItem className="pt-2 pb-4"> - <AccordionHead showArrow={false}> - <span className="font-semibold">Email template</span> - </AccordionHead> - <AccordionBody> - <TextEditor - editorState={editorState} - setEditorState={setEditorState} - showToolbar={true} - placeholder="Start typing your report template..." - /> - </AccordionBody> - </AccordionItem> - </Accordion> - </div> - ) + <Button onClick={handleSave}>Save Template</Button> + </div> + </AccordionBody> + </AccordionItem> + </Accordion> + </div> ); -} +} \ No newline at end of file -- GitLab