From 5899bb0a121ac4a249fa3ac9358991ed449ef264 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 7 Jan 2025 00:26:31 +0700 Subject: [PATCH] feat: no global edit --- .../providers/high.order.provider.tsx | 956 +++++++++--------- 1 file changed, 490 insertions(+), 466 deletions(-) diff --git a/apps/frontend/src/components/launches/providers/high.order.provider.tsx b/apps/frontend/src/components/launches/providers/high.order.provider.tsx index 57c11780..a2d67a16 100644 --- a/apps/frontend/src/components/launches/providers/high.order.provider.tsx +++ b/apps/frontend/src/components/launches/providers/high.order.provider.tsx @@ -1,11 +1,18 @@ 'use client'; import React, { - FC, Fragment, ReactNode, useCallback, useEffect, useMemo, useState, ClipboardEvent, memo + FC, + Fragment, + ReactNode, + useCallback, + useEffect, + useMemo, + useState, + ClipboardEvent, + memo, } from 'react'; import { Button } from '@gitroom/react/form/button'; import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; -import MDEditor, { commands } from '@uiw/react-md-editor'; import { useHideTopEditor } from '@gitroom/frontend/components/launches/helpers/use.hide.top.editor'; import { useValues } from '@gitroom/frontend/components/launches/helpers/use.values'; import { FormProvider } from 'react-hook-form'; @@ -18,7 +25,6 @@ import { import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component'; import { createPortal } from 'react-dom'; import clsx from 'clsx'; -import { newImage } from '@gitroom/frontend/components/launches/helpers/new.image.component'; import { postSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector'; import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow'; import { arrayMoveImmutable } from 'array-move'; @@ -31,7 +37,6 @@ import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core'; import { AddPostButton } from '@gitroom/frontend/components/launches/add.post.button'; import { GeneralPreviewComponent } from '@gitroom/frontend/components/launches/general.preview.component'; import { capitalize } from 'lodash'; -import { useModals } from '@mantine/modals'; import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader'; import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; import { DropFiles } from '@gitroom/frontend/components/layout/drop.files'; @@ -82,506 +87,525 @@ export const withProvider = function ( ) => Promise, maximumCharacters?: number | ((settings: any) => number) ) { - return memo((props: { - identifier: string; - id: string; - value: Array<{ - content: string; - id?: string; - image?: Array<{ path: string; id: string }>; - }>; - hideMenu?: boolean; - show: boolean; - }) => { - const existingData = useExistingData(); - const { allIntegrations, integration, date } = useIntegration(); - const [showLinkedinPopUp, setShowLinkedinPopUp] = useState(false); - const [uploading, setUploading] = useState(false); - const fetch = useFetch(); - - useCopilotReadable({ - description: - integration?.type === 'social' - ? 'force content always in MD format' - : 'force content always to be fit to social media', - value: '', - }); - const [editInPlace, setEditInPlace] = useState(!!existingData.integration); - const [InPlaceValue, setInPlaceValue] = useState< - Array<{ - id?: string; + return memo( + (props: { + identifier: string; + id: string; + value: Array<{ content: string; - image?: Array<{ id: string; path: string }>; - }> - >( - // @ts-ignore - existingData.integration - ? existingData.posts.map((p) => ({ - id: p.id, - content: p.content, - image: p.image, - })) - : [{ content: '' }] - ); - const [showTab, setShowTab] = useState(0); + id?: string; + image?: Array<{ path: string; id: string }>; + }>; + hideMenu?: boolean; + show: boolean; + }) => { + const existingData = useExistingData(); + const { allIntegrations, integration, date } = useIntegration(); + const [showLinkedinPopUp, setShowLinkedinPopUp] = useState(false); + const [uploading, setUploading] = useState(false); + const fetch = useFetch(); - const Component = useMemo(() => { - return SettingsComponent ? SettingsComponent : () => <>; - }, [SettingsComponent]); - - // in case there is an error on submit, we change to the settings tab for the specific provider - useMoveToIntegrationListener( - [props.id], - true, - ({ identifier, toPreview }) => { - if (identifier === props.id) { - setShowTab(toPreview ? 1 : 2); - } - } - ); - - // this is a smart function, it updates the global value without updating the states (too heavy) and set the settings validation - const form = useValues( - existingData.settings, - props.id, - props.identifier, - editInPlace ? InPlaceValue : props.value, - dto, - checkValidity, - !maximumCharacters - ? undefined - : typeof maximumCharacters === 'number' - ? maximumCharacters - : maximumCharacters(JSON.parse(integration?.additionalSettings || '[]')) - ); - - // change editor value - const changeValue = useCallback( - (index: number) => (newValue: string) => { - return setInPlaceValue((prev) => { - prev[index].content = newValue; - return [...prev]; - }); - }, - [InPlaceValue] - ); - - const changeImage = useCallback( - (index: number) => - (newValue: { - target: { name: string; value?: Array<{ id: string; path: string }> }; - }) => { - return setInPlaceValue((prev) => { - prev[index].image = newValue.target.value; - return [...prev]; - }); - }, - [InPlaceValue] - ); - - // add another local editor - const addValue = useCallback( - (index: number) => () => { - setInPlaceValue((prev) => { - return prev.reduce((acc, p, i) => { - acc.push(p); - if (i === index) { - acc.push({ content: '' }); - } - - return acc; - }, [] as Array<{ content: string }>); - }); - }, - [] - ); - - const changePosition = useCallback( - (index: number) => (type: 'up' | 'down') => { - if (type === 'up' && index !== 0) { - setInPlaceValue((prev) => { - return arrayMoveImmutable(prev, index, index - 1); - }); - } else if (type === 'down') { - setInPlaceValue((prev) => { - return arrayMoveImmutable(prev, index, index + 1); - }); - } - }, - [] - ); - - // Delete post - const deletePost = useCallback( - (index: number) => async () => { - if ( - !(await deleteDialog( - 'Are you sure you want to delete this post?', - 'Yes, delete it!' - )) - ) { - return; - } - setInPlaceValue((prev) => { - prev.splice(index, 1); - return [...prev]; - }); - }, - [InPlaceValue] - ); - - // This is a function if we want to switch from the global editor to edit in place - const changeToEditor = useCallback(async () => { - if ( - !(await deleteDialog( - !editInPlace - ? 'Are you sure you want to edit only this?' - : 'Are you sure you want to revert it back to global editing?', - 'Yes, edit in place!' - )) - ) { - return false; - } - - setEditInPlace(!editInPlace); - setInPlaceValue( - editInPlace - ? [{ content: '' }] - : props.value.map((p) => ({ + useCopilotReadable({ + description: + integration?.type === 'social' + ? 'force content always in MD format' + : 'force content always to be fit to social media', + value: '', + }); + const [editInPlace, setEditInPlace] = useState( + !!existingData.integration + ); + const [InPlaceValue, setInPlaceValue] = useState< + Array<{ + id?: string; + content: string; + image?: Array<{ id: string; path: string }>; + }> + >( + // @ts-ignore + existingData.integration + ? existingData.posts.map((p) => ({ id: p.id, content: p.content, image: p.image, })) + : [{ content: '' }] ); - }, [props.value, editInPlace]); + const [showTab, setShowTab] = useState(0); - useCopilotAction({ - name: editInPlace - ? 'switchToGlobalEdit' - : `editInPlace_${integration?.identifier}`, - description: editInPlace - ? 'Switch to global editing' - : `Edit only ${integration?.identifier} this, if you want a different identifier, you have to use setSelectedIntegration first`, - handler: async () => { - await changeToEditor(); - }, - }); + const Component = useMemo(() => { + return SettingsComponent ? SettingsComponent : () => <>; + }, [SettingsComponent]); - const tagPersonOrCompany = useCallback( - (integration: string, editor: (value: string) => void) => () => { - setShowLinkedinPopUp( - { - editor(tag); - }} - id={integration} - onClose={() => setShowLinkedinPopUp(false)} - /> - ); - }, - [] - ); + // in case there is an error on submit, we change to the settings tab for the specific provider + useMoveToIntegrationListener( + [props.id], + true, + ({ identifier, toPreview }) => { + if (identifier === props.id) { + setShowTab(toPreview ? 1 : 2); + } + } + ); - const uppy = useUppyUploader({ - onUploadSuccess: () => { - /**empty**/ - }, - allowedFileTypes: 'image/*,video/mp4', - }); + // this is a smart function, it updates the global value without updating the states (too heavy) and set the settings validation + const form = useValues( + existingData.settings, + props.id, + props.identifier, + editInPlace ? InPlaceValue : props.value, + dto, + checkValidity, + !maximumCharacters + ? undefined + : typeof maximumCharacters === 'number' + ? maximumCharacters + : maximumCharacters( + JSON.parse(integration?.additionalSettings || '[]') + ) + ); - const pasteImages = useCallback( - (index: number, currentValue: any[], isFile?: boolean) => { - return async (event: ClipboardEvent | File[]) => { - // @ts-ignore - const clipboardItems = isFile - ? // @ts-ignore - event.map((p) => ({ kind: 'file', getAsFile: () => p })) - : // @ts-ignore - event.clipboardData?.items; // Ensure clipboardData is available - if (!clipboardItems) { + // change editor value + const changeValue = useCallback( + (index: number) => (newValue: string) => { + return setInPlaceValue((prev) => { + prev[index].content = newValue; + return [...prev]; + }); + }, + [InPlaceValue] + ); + + const changeImage = useCallback( + (index: number) => + (newValue: { + target: { + name: string; + value?: Array<{ id: string; path: string }>; + }; + }) => { + return setInPlaceValue((prev) => { + prev[index].image = newValue.target.value; + return [...prev]; + }); + }, + [InPlaceValue] + ); + + // add another local editor + const addValue = useCallback( + (index: number) => () => { + setInPlaceValue((prev) => { + return prev.reduce((acc, p, i) => { + acc.push(p); + if (i === index) { + acc.push({ content: '' }); + } + + return acc; + }, [] as Array<{ content: string }>); + }); + }, + [] + ); + + const changePosition = useCallback( + (index: number) => (type: 'up' | 'down') => { + if (type === 'up' && index !== 0) { + setInPlaceValue((prev) => { + return arrayMoveImmutable(prev, index, index - 1); + }); + } else if (type === 'down') { + setInPlaceValue((prev) => { + return arrayMoveImmutable(prev, index, index + 1); + }); + } + }, + [] + ); + + // Delete post + const deletePost = useCallback( + (index: number) => async () => { + if ( + !(await deleteDialog( + 'Are you sure you want to delete this post?', + 'Yes, delete it!' + )) + ) { return; } + setInPlaceValue((prev) => { + prev.splice(index, 1); + return [...prev]; + }); + }, + [InPlaceValue] + ); - const files: File[] = []; + // This is a function if we want to switch from the global editor to edit in place + const changeToEditor = useCallback(async () => { + if ( + !(await deleteDialog( + !editInPlace + ? 'Are you sure you want to edit only this?' + : 'Are you sure you want to revert it back to global editing?', + 'Yes, edit in place!' + )) + ) { + return false; + } - // @ts-ignore - for (const item of clipboardItems) { - console.log(item); - if (item.kind === 'file') { - const file = item.getAsFile(); - if (file) { - const isImage = file.type.startsWith('image/'); - const isVideo = file.type.startsWith('video/'); - if (isImage || isVideo) { - files.push(file); // Collect images or videos + setEditInPlace(!editInPlace); + setInPlaceValue( + editInPlace + ? [{ content: '' }] + : props.value.map((p) => ({ + id: p.id, + content: p.content, + image: p.image, + })) + ); + }, [props.value, editInPlace]); + + useCopilotAction({ + name: editInPlace + ? 'switchToGlobalEdit' + : `editInPlace_${integration?.identifier}`, + description: editInPlace + ? 'Switch to global editing' + : `Edit only ${integration?.identifier} this, if you want a different identifier, you have to use setSelectedIntegration first`, + handler: async () => { + await changeToEditor(); + }, + }); + + const tagPersonOrCompany = useCallback( + (integration: string, editor: (value: string) => void) => () => { + setShowLinkedinPopUp( + { + editor(tag); + }} + id={integration} + onClose={() => setShowLinkedinPopUp(false)} + /> + ); + }, + [] + ); + + const uppy = useUppyUploader({ + onUploadSuccess: () => { + /**empty**/ + }, + allowedFileTypes: 'image/*,video/mp4', + }); + + const pasteImages = useCallback( + (index: number, currentValue: any[], isFile?: boolean) => { + return async (event: ClipboardEvent | File[]) => { + // @ts-ignore + const clipboardItems = isFile + ? // @ts-ignore + event.map((p) => ({ kind: 'file', getAsFile: () => p })) + : // @ts-ignore + event.clipboardData?.items; // Ensure clipboardData is available + if (!clipboardItems) { + return; + } + + const files: File[] = []; + + // @ts-ignore + for (const item of clipboardItems) { + console.log(item); + if (item.kind === 'file') { + const file = item.getAsFile(); + if (file) { + const isImage = file.type.startsWith('image/'); + const isVideo = file.type.startsWith('video/'); + if (isImage || isVideo) { + files.push(file); // Collect images or videos + } } } } - } - if (files.length === 0) { - return; - } - - setUploading(true); - const lastValues = [...currentValue]; - for (const file of files) { - uppy.addFile(file); - const upload = await uppy.upload(); - uppy.clear(); - if (upload?.successful?.length) { - lastValues.push(upload?.successful[0]?.response?.body?.saved!); - changeImage(index)({ - target: { - name: 'image', - value: [...lastValues], - }, - }); + if (files.length === 0) { + return; } - } - setUploading(false); - }; - }, - [changeImage] - ); - const getInternalPlugs = useCallback(async () => { + setUploading(true); + const lastValues = [...currentValue]; + for (const file of files) { + uppy.addFile(file); + const upload = await uppy.upload(); + uppy.clear(); + if (upload?.successful?.length) { + lastValues.push(upload?.successful[0]?.response?.body?.saved!); + changeImage(index)({ + target: { + name: 'image', + value: [...lastValues], + }, + }); + } + } + setUploading(false); + }; + }, + [changeImage] + ); + + const getInternalPlugs = useCallback(async () => { + return ( + await fetch(`/integrations/${props.identifier}/internal-plugs`) + ).json(); + }, [props.identifier]); + + const { data } = useSWR(`internal-${props.identifier}`, getInternalPlugs); + + // this is a trick to prevent the data from being deleted, yet we don't render the elements + if (!props.show) { + return null; + } + return ( - await fetch(`/integrations/${props.identifier}/internal-plugs`) - ).json(); - }, [props.identifier]); - - const { data } = useSWR(`internal-${props.identifier}`, getInternalPlugs); - - // this is a trick to prevent the data from being deleted, yet we don't render the elements - if (!props.show) { - return null; - } - - return ( - - setShowTab(0)} /> - {showLinkedinPopUp ? showLinkedinPopUp : null} -
- {!props.hideMenu && ( -
-
- -
- {(!!SettingsComponent || !!data?.internalPlugs?.length) && ( + + setShowTab(0)} /> + {showLinkedinPopUp ? showLinkedinPopUp : null} +
+ {!props.hideMenu && ( +
- )} -
- -
-
- )} - {editInPlace && - createPortal( - - {uploading && ( -
- + {(!!SettingsComponent || !!data?.internalPlugs?.length) && ( +
+
)} -
- {!existingData?.integration && ( -
- You are now editing only {integration?.name} ( - {capitalize(integration?.identifier.replace('-', ' '))}) + {!existingData.integration && ( +
+ +
+ )} +
+ )} + {editInPlace && + createPortal( + + {uploading && ( +
+
)} - {InPlaceValue.map((val, index) => ( - -
-
-
- {(integration?.identifier === 'linkedin' || - integration?.identifier === 'linkedin-page') && ( - + )} + - Tag a company - - )} - - 1 ? 200 : 250} - value={val.content} - commands={[ - // ...commands - // .getCommands() - // .filter((f) => f.name !== 'image'), - // newImage, - postSelector(date), - ...linkedinCompany( - integration?.identifier!, - integration?.id! - ), - ]} - preview="edit" - onPaste={pasteImages(index, val.image || [])} - // @ts-ignore - onChange={changeValue(index)} - /> - - {(!val.content || val.content.length < 6) && ( -
- The post should be at least 6 characters long -
- )} -
-
- 1 ? 200 : 250} + value={val.content} + commands={[ + // ...commands + // .getCommands() + // .filter((f) => f.name !== 'image'), + // newImage, + postSelector(date), + ...linkedinCompany( + integration?.identifier!, + integration?.id! + ), + ]} + preview="edit" + onPaste={pasteImages(index, val.image || [])} + // @ts-ignore + onChange={changeValue(index)} /> -
-
- {InPlaceValue.length > 1 && ( -
-
- - - + + {(!val.content || val.content.length < 6) && ( +
+ The post should be at least 6 characters long +
+ )} +
+
+ +
+
+ {InPlaceValue.length > 1 && ( +
+
+ + + +
+
+ Delete Post +
-
- Delete Post -
-
- )} + )} +
-
-
- +
+ +
-
-
- -
- - ))} -
- , - document.querySelector('#renderEditor')! - )} - {(showTab === 0 || showTab === 2) && ( -
- - {!!data?.internalPlugs?.length && ( - +
+ +
+ + ))} +
+ , + document.querySelector('#renderEditor')! )} -
- )} - {showTab === 0 && ( -
- - {(editInPlace ? InPlaceValue : props.value) - .map((p) => p.content) - .join('').length ? ( - CustomPreviewComponent ? ( - - ) : ( - - ) - ) : ( - <>No Content Yet + {(showTab === 0 || showTab === 2) && ( +
+ + {!!data?.internalPlugs?.length && ( + )} - -
- )} -
- - ); - }); +
+ )} + {showTab === 0 && ( +
+ + {(editInPlace ? InPlaceValue : props.value) + .map((p) => p.content) + .join('').length ? ( + CustomPreviewComponent ? ( + + ) : ( + + ) + ) : ( + <>No Content Yet + )} + +
+ )} +
+ + ); + } + ); };