diff --git a/.vscode/settings.json b/.vscode/settings.json index a68fcf822..708a19cee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ - "Auths" + "Auths", + "testid" ], "[javascript]": { "editor.formatOnSave": true, diff --git a/package.json b/package.json index 6c9821560..d42db0dbb 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.16.7", "@babel/register": "^7.0.0", + "@types/ramda": "^0.28.15", "babel-eslint": "^7.1.0", "babel-loader": "^8.2.4", "babel-plugin-add-module-exports": "^1.0.4", @@ -126,6 +127,7 @@ "prettify-xml": "^1.2.0", "pug-cli": "^1.0.0-alpha6", "querystring": "^0.2.1", + "ramda": "^0.28.0", "react-file-drop": "^3.1.5" }, "workspaces": [ diff --git a/packages/firecamp-core/package.json b/packages/firecamp-core/package.json index 9fb5ab15f..717076bab 100644 --- a/packages/firecamp-core/package.json +++ b/packages/firecamp-core/package.json @@ -87,7 +87,7 @@ "react-dnd": "^16.0.0", "react-dnd-html5-backend": "^16.0.1", "react-dom": "17.0.2", - "react-error-boundary": "^3.1.3", + "react-error-boundary": "^3.1.4", "react-file-drop": "^3.1.5", "react-ga": "^3.1.2", "react-hook-form": "^6.8.1", diff --git a/packages/firecamp-core/src/components/activity-bar/explorer/Explorer.tsx b/packages/firecamp-core/src/components/activity-bar/explorer/Explorer.tsx index bd119a7c6..32a062275 100644 --- a/packages/firecamp-core/src/components/activity-bar/explorer/Explorer.tsx +++ b/packages/firecamp-core/src/components/activity-bar/explorer/Explorer.tsx @@ -23,19 +23,18 @@ import { VscNewFolder } from '@react-icons/all-files/vsc/VscNewFolder'; // import { VscFileSymlinkFile } from '@react-icons/all-files/vsc/VscFileSymlinkFile'; import { VscFolder } from '@react-icons/all-files/vsc/VscFolder'; -import { useTabStore } from '../../../store/tab'; import { useWorkspaceStore } from '../../../store/workspace'; import { WorkspaceCollectionsProvider } from './WorkspaceCollectionsProvider'; import treeRenderer from './treeItemRenderer'; import AppService from '../../../services/app'; import { RE } from '../../../types' +import { platformEmitter as emitter } from '../../../services/platform-emitter' +import { EPlatformTabs } from '../../../services/platform-emitter/events' const Explorer: FC = () => { const environmentRef = useRef(); const treeRef = useRef(); - const { openSavedTab } = useTabStore((s) => ({ openSavedTab: s.open.saved }), shallow); - let { workspace, explorer, @@ -115,31 +114,30 @@ const Explorer: FC = () => { }; const _openReqInTab = (request: any) => { - // console.log(`node`, request); - openSavedTab(request); + console.log(`node`, request); + emitter.emit(EPlatformTabs.openSaved, request); }; - const _onNodeSelect = (nodeIdxs: TreeItemIndex[]) => { - // console.log({ nodeIdxs }); + const _onNodeSelect = (nodeIndexes: TreeItemIndex[]) => { + // console.log({ nodeIndexes }); // return - let nodeIndex = nodeIdxs[0]; - let colItem = [...collections, ...folders, ...requests].find( + let nodeIndex = nodeIndexes[0]; + let nodeItem = [...collections, ...folders, ...requests].find( (c) => c._meta.id == nodeIndex ); - // console.log({ colItem }); + // console.log({ nodeItem }); if ( - colItem && + nodeItem && [ ERequestTypes.Rest, ERequestTypes.GraphQL, ERequestTypes.SocketIO, ERequestTypes.WebSocket, - ].includes(colItem.meta.type) + ].includes(nodeItem.meta.type) ) { - // console.log({ colItem }); - - _openReqInTab(colItem); + // console.log({ nodeItem }); + _openReqInTab(nodeItem); } }; diff --git a/packages/firecamp-core/src/components/common/environment/environment-widget/EnvironmentDD.tsx b/packages/firecamp-core/src/components/common/environment/environment-widget/EnvironmentDD.tsx index 327f4feff..05bcd8cf5 100644 --- a/packages/firecamp-core/src/components/common/environment/environment-widget/EnvironmentDD.tsx +++ b/packages/firecamp-core/src/components/common/environment/environment-widget/EnvironmentDD.tsx @@ -52,20 +52,7 @@ const EnvironmentDD: FC = ({ ? 'Manage Workspace Environment' : 'Manage Collection Environment', onClick: () => { - // if (scope === EEnvironmentScope.Workspace) { - // F.appStore.environment.update.activeProject('global'); - // F.ModalService.open( - // EModals.WORKSPACE_SETTING, - // EWorkspaceSettingTabs.ENVIRONMENT - // ); - // } else { - // F.appStore.environment.update.activeProject(collectionId); - // F.ModalService.open( - // EModals.PROJECT_SETTING, - // ECollectionSettingTabs.ENVIRONMENT, - // { collectionId } - // ); - // } + }, }, ]; diff --git a/packages/firecamp-core/src/components/common/environment/environment-widget/WorkspaceEnvDD.tsx b/packages/firecamp-core/src/components/common/environment/environment-widget/WorkspaceEnvDD.tsx index bff43823c..692ef3907 100644 --- a/packages/firecamp-core/src/components/common/environment/environment-widget/WorkspaceEnvDD.tsx +++ b/packages/firecamp-core/src/components/common/environment/environment-widget/WorkspaceEnvDD.tsx @@ -63,20 +63,7 @@ const WorkspaceEnvDD: FC = ({ ? 'Manage Workspace Environment' : 'Manage Collection Environment', onClick: () => { - // if (scope === EEnvironmentScope.Workspace) { - // F.appStore.environment.update.activeProject('global'); - // F.ModalService.open( - // EModals.WORKSPACE_SETTING, - // EWorkspaceSettingTabs.ENVIRONMENT - // ); - // } else { - // F.appStore.environment.update.activeProject(id); - // F.ModalService.open( - // EModals.PROJECT_SETTING, - // ECollectionSettingTabs.ENVIRONMENT, - // { id } - // ); - // } + }, }, ]; diff --git a/packages/firecamp-core/src/components/common/error-boundary/ErrorPopup.tsx b/packages/firecamp-core/src/components/common/error-boundary/ErrorPopup.tsx index 760591b89..73a7b008c 100644 --- a/packages/firecamp-core/src/components/common/error-boundary/ErrorPopup.tsx +++ b/packages/firecamp-core/src/components/common/error-boundary/ErrorPopup.tsx @@ -1,15 +1,10 @@ import { FC, useState } from 'react'; +import { FallbackProps } from 'react-error-boundary'; import { Modal } from '@firecamp/ui-kit'; import './ErrorPopup.sass'; -const ErrorPopup: FC = ({ - message = `An unexpected error occurs. -We'll surely fix it out for you soon. We appreciate your patience.`, - onClose = () => {}, -}) => { - // console.log(`in error popup`, message, isOpen) - +const ErrorPopup: FC = ({ error }) => { const bg = { modal: { background: '#c84a1782', @@ -21,7 +16,7 @@ We'll surely fix it out for you soon. We appreciate your patience.`, let _onClose = async () => { // on close, refresh app - onClose(); + // onClose(); // await initApp(); toggleOpen(false); }; @@ -40,24 +35,10 @@ We'll surely fix it out for you soon. We appreciate your patience.`, paddingRight: '60px', }} > - {message || ''} + {error.message || ''} ); }; export default ErrorPopup; - -/** - * Handle firecamp errors and show popup menu to instruct users about the same - */ -interface IErrorPopup { - /** - * Error message - */ - message?: string; - /** - * Close error popup - */ - onClose?: () => void; -} diff --git a/packages/firecamp-core/src/components/common/realtime/Realtime.tsx b/packages/firecamp-core/src/components/common/realtime/Realtime.tsx index 1566ec121..7f661ff3f 100644 --- a/packages/firecamp-core/src/components/common/realtime/Realtime.tsx +++ b/packages/firecamp-core/src/components/common/realtime/Realtime.tsx @@ -1,37 +1,44 @@ import { FC, useEffect } from 'react'; import { Realtime } from '@firecamp/cloud-apis'; -import { prepareEventNameForRequestPull } from '../../../types'; -import { platformEmitter } from '../../../services/platform-emitter'; -import { useWorkspaceStore } from '../../../store/workspace' +import { platformEmitter as emitter } from '../../../services/platform-emitter'; +import { useWorkspaceStore } from '../../../store/workspace'; +import { + EPlatformTabs, + prepareEventNameForRequestPull, +} from '../../../services/platform-emitter/events'; +import { useTabStore } from '../../../store/tab'; +import PreComp from '../../tabs/header/PreComp'; +import { TId } from '@firecamp/types'; const RealtimeEventManager: FC = () => { + const { open, close } = useTabStore.getState(); - const { - onCreateCollection, - onUpdateCollection, - onDeleteCollection, - onCreateFolder, - onUpdateFolder, - onDeleteFolder, - onCreateRequest, - onUpdateRequest, - onDeleteRequest - } = useWorkspaceStore.getState(); + const { + onCreateCollection, + onUpdateCollection, + onDeleteCollection, + onCreateFolder, + onUpdateFolder, + onDeleteFolder, + onCreateRequest, + onUpdateRequest, + onDeleteRequest, + } = useWorkspaceStore.getState(); /** handle realtime request changes */ useEffect(() => { const onRequestChange = () => { Realtime.onRequestChanges((payload) => { console.log({ onRequestChanges: payload }); - platformEmitter.emit( + emitter.emit( prepareEventNameForRequestPull(payload.request_id), payload.actions ); }); }; - platformEmitter.on('socket.connected', onRequestChange); + emitter.on('socket.connected', onRequestChange); return () => { - platformEmitter.off('socket.connected', onRequestChange); + emitter.off('socket.connected', onRequestChange); }; }, []); @@ -39,70 +46,112 @@ const RealtimeEventManager: FC = () => { useEffect(() => { const onExplorerItemInsert = ({ type, payload }) => { console.log(type, payload); - switch(type) { - case "collection": - onCreateCollection(payload); - break; - case "folder": - onCreateFolder(payload); - break; - case "request": - onCreateRequest(payload); - break; - } + switch (type) { + case 'collection': + onCreateCollection(payload); + break; + case 'folder': + onCreateFolder(payload); + break; + case 'request': + onCreateRequest(payload); + break; + } }; const onExplorerItemUpdate = ({ type, payload }) => { console.log(type, payload); - switch(type) { - case "collection": - onUpdateCollection(payload); - break; - case "folder": - onUpdateFolder(payload); - break; - case "request": - onUpdateRequest(payload); - break; - } + switch (type) { + case 'collection': + onUpdateCollection(payload); + break; + case 'folder': + onUpdateFolder(payload); + break; + case 'request': + onUpdateRequest(payload); + break; + } }; const onExplorerItemDelete = ({ type, payload }) => { console.log(type, payload); - switch(type) { - case "collection": - onDeleteCollection(payload); - break; - case "folder": - onDeleteFolder(payload); - break; - case "request": - onDeleteRequest(payload); - break; - } + switch (type) { + case 'collection': + onDeleteCollection(payload); + break; + case 'folder': + onDeleteFolder(payload); + break; + case 'request': + onDeleteRequest(payload); + break; + } }; const listenExplorerChanges = () => { - console.log('socket:connected'); + console.log('socket:connected'); Realtime.listenExplorerItemInsert(onExplorerItemInsert); Realtime.listenExplorerItemUpdate(onExplorerItemUpdate); Realtime.listenExplorerItemDelete(onExplorerItemDelete); }; - const listenOffExplorerChanges = () => { - console.log('socket:disconnected'); - Realtime.listenOffExplorerItemInsert(); - Realtime.listenOffExplorerItemUpdate(); - Realtime.listenOffExplorerItemDelete(); - }; + const listenOffExplorerChanges = () => { + console.log('socket:disconnected'); + Realtime.listenOffExplorerItemInsert(); + Realtime.listenOffExplorerItemUpdate(); + Realtime.listenOffExplorerItemDelete(); + }; - platformEmitter.on('socket.connected', listenExplorerChanges); - platformEmitter.on('socket.disconnected', listenOffExplorerChanges); + emitter.on('socket.connected', listenExplorerChanges); + emitter.on('socket.disconnected', listenOffExplorerChanges); return () => { - platformEmitter.off('socket.connected'); + emitter.off('socket.connected'); // TODO: unsubscribe explorer changes on unmount server socket }; }, []); + // handle platform events + useEffect(() => { + emitter.on(EPlatformTabs.openNew, (type: string) => { + const [tab, orders] = open.new(type, true); + emitter.emit(EPlatformTabs.opened, [ + { + ...tab, + name: tab.name || tab.request.meta.name, + preComp: ( + + ), + dotIndicator: tab.meta?.hasChange === true, + }, + orders, + ]); + }); + + emitter.on(EPlatformTabs.openSaved, (request: any) => { + const [ tab, orders ] = open.saved(request); + console.log(tab, "opened tab") + if (!tab) return; + emitter.emit(EPlatformTabs.opened, [{ + ...tab, + name: tab.name || request.meta.name, + preComp: ( + + ), + dotIndicator: tab.meta?.hasChange === true, + }, orders]); + }); + + emitter.on(EPlatformTabs.close, (tabId_s: TId | TId[]) => { + close.byIds(Array.isArray(tabId_s) ? tabId_s : [tabId_s]); + emitter.emit(EPlatformTabs.closed, tabId_s); + }); + + return () => { + emitter.off(EPlatformTabs.openNew); + emitter.off(EPlatformTabs.openSaved); + }; + }, []); + return <>; }; diff --git a/packages/firecamp-core/src/components/common/save/SavePopover.tsx b/packages/firecamp-core/src/components/common/save/SavePopover.tsx index 4eda56a70..d13775257 100644 --- a/packages/firecamp-core/src/components/common/save/SavePopover.tsx +++ b/packages/firecamp-core/src/components/common/save/SavePopover.tsx @@ -1,8 +1,6 @@ import { FC, useCallback, useRef, useState } from 'react'; import { Button, - - Input, TextArea, Popover, @@ -13,7 +11,7 @@ import { Tree, UncontrolledTreeEnvironment } from '@firecamp/ui-kit/src/tree'; import { useWorkspaceStore } from '../../../store/workspace'; import { CollectionDataProvider } from './collection/CollectionDataProvider'; import treeRenderer from './collection/treeItemRenderer'; -import { ITabMeta } from '../../tabs/types/tab'; +import { IRequestTab } from '../../tabs/types/tab'; //TODO: need to discuss this, why these two regex const ptn = /^(?!-).*^(?!_).*^(?!\.).*^(?!\().*^(?!\)).*([a-zA-Z0-9-_.\W\s]+)/i; @@ -263,7 +261,7 @@ export interface ISavePopover { /** * Request tab meta */ - tabMeta: ITabMeta; + tabMeta: IRequestTab['meta']; /** * Save modal meta diff --git a/packages/firecamp-core/src/components/containers/TabsContainer.tsx b/packages/firecamp-core/src/components/containers/TabsContainer.tsx index 9ec05db59..5c116a752 100644 --- a/packages/firecamp-core/src/components/containers/TabsContainer.tsx +++ b/packages/firecamp-core/src/components/containers/TabsContainer.tsx @@ -3,12 +3,10 @@ import { Row, RootContainer, Column } from '@firecamp/ui-kit'; import _cloneDeep from 'lodash/cloneDeep'; import shallow from 'zustand/shallow'; import { _object } from '@firecamp/utils'; -import { ERequestTypes, TId } from '@firecamp/types'; import { _misc } from '@firecamp/utils'; import { useTabStore } from '../../store/tab'; import { useEnvStore, IEnvironmentStore } from '../../store/environment'; -import { ITab, ITabFns, ITabMeta } from '../tabs/types/tab'; import TabContainerHeader from '../tabs/TabContainerHeader'; import TabContainerBody from '../tabs/TabContainerBody'; @@ -51,85 +49,33 @@ const TabsContainer: FC = () => { } }, [tabs]); - const _tabFns: ITabFns = { - setActive: (tabId) => { - tabFns.update.activeTab(tabId); - }, - - open: (tabType: string, subType: string) => { - if (!tabType) { - if (tabs?.length === 0) tabType = ERequestTypes.Rest; - else { - let tab; - if ((activeTab || activeTab) === 'home') { - tab = tabs?.[tabs?.length - 1]; - } else { - tab = (tabs || []).find((tab) => tab.id === activeTab || activeTab); - } - tabType = tab?.type; - subType = tab?.subType; - } - } - if (tabType) tabFns.open.new(tabType, true, subType); - }, - + const _tabFns = { close: (e: any, tabId: string, doSave: boolean) => { - if (e) e?.stopPropagation(); + if (e) e.stopPropagation(); // console.log(tabId, doSave) if (doSave) { - //Todo: this is hack for now, we should close it by redux or calling func + //Todo: this is hack for now, we should close it by action or calling func document.getElementById(`save-request-${tabId}`).click(); //click on Save button programmatically } tabFns.close.active(tabId); }, + reorder: (dragIndex: number, hoverIndex: number) => { tabFns.reorder(dragIndex, hoverIndex); }, - closeAll: (e) => { - if (e) e?.preventDefault(); - tabFns.close.all(); - }, - closeAllSaved: (e) => { - if (e) e?.preventDefault(); - - tabFns.close.allSaved(); - }, - closeAllFresh: (e) => { - if (e) e?.preventDefault(); - tabFns.close.allFresh(); - }, - closeAllExceptActive: (e) => { - if (e) e?.preventDefault(); - tabFns.close.allExceptActive(); - }, save: () => { document.getElementById(`save-request-${activeTab}`).click(); }, - updateMeta: (tabId: TId, meta: ITabMeta) => { - // console.log({ tabId, meta }); - tabFns.update.meta(tabId, meta); - }, - updateRootKeys: (tabId: TId, updatedTab: Partial) => { - tabFns.update.rootKeys(tabId, updatedTab); - }, }; return ( - - + + diff --git a/packages/firecamp-core/src/components/modals/collection-setting/body/environment/Environment.tsx b/packages/firecamp-core/src/components/modals/collection-setting/body/environment/Environment.tsx index d15381e37..c8c26f745 100644 --- a/packages/firecamp-core/src/components/modals/collection-setting/body/environment/Environment.tsx +++ b/packages/firecamp-core/src/components/modals/collection-setting/body/environment/Environment.tsx @@ -324,26 +324,10 @@ const Environment: FC = ({ update: async (payload = {}, envId, collectionId) => { if (!activeEnvCard || !activeEnvCard._meta.id) return; - - // await F.appStore.environment.update.environment( - // { - // collectionId: collectionId || collectionId, - // envId: envId || activeEnvCard._meta.id || '', - // ...payload, - // }, - // { sync: true, cloudSync: true } - // ); }, remove: async (environment = {}) => { - // await F.appStore.environment.remove.environment( - // { - // collectionId: collectionId, - // envId: environment._meta.id || '', - // updateDB: true, - // }, - // { sync: true, cloudSync: true } - // ); + }, }; diff --git a/packages/firecamp-core/src/components/sidebar/EnvSidebar.tsx b/packages/firecamp-core/src/components/sidebar/EnvSidebar.tsx index 754d110e2..2eb8d944e 100644 --- a/packages/firecamp-core/src/components/sidebar/EnvSidebar.tsx +++ b/packages/firecamp-core/src/components/sidebar/EnvSidebar.tsx @@ -41,7 +41,7 @@ const EnvSidebar: FC = ({ expanded }) => { const { tab, activeTab } = useTabStore( (s: any) => ({ - tab: s.list?.find((t) => t.id === s.activeTab) || {}, + tab: s.list[s.activeTab] || {}, activeTab: s.activeTab, }), shallow diff --git a/packages/firecamp-core/src/components/tabs/TabBody.tsx b/packages/firecamp-core/src/components/tabs/TabBody.tsx index 13c2980bb..9c10df4b2 100644 --- a/packages/firecamp-core/src/components/tabs/TabBody.tsx +++ b/packages/firecamp-core/src/components/tabs/TabBody.tsx @@ -27,19 +27,20 @@ const GraphQL = lazy(() => import EnvironmentWidget from '../common/environment/environment-widget/EnvironmentWidget'; import ErrorPopup from '../common/error-boundary/ErrorPopup'; // import SavePopover from '../common/save/SavePopover'; -import { ITabProps } from './types'; +import { IRequestTabProps } from './types'; import * as platformContext from '../../services/platform-context'; - -import { IPlatformStore, usePlatformStore } from '../../store/platform'; +import { usePlatformStore } from '../../store/platform'; import AppService from '../../services/app'; +import { useTabStore } from '../../store/tab' -const TabBody = ({ tabObj, index, tabFns, activeTab }) => { +const TabBody = ({ tabObj, index, activeTab }) => { if (!tabObj || index === -1) { return ; } const { getFirecampAgent } = usePlatformStore.getState(); + const { changeActiveTab, close } = useTabStore.getState(); if ( [ @@ -55,7 +56,7 @@ const TabBody = ({ tabObj, index, tabFns, activeTab }) => { }); } - const tabProps: ITabProps = useMemo(() => { + const tabProps: IRequestTabProps = useMemo(() => { return { index: index, tab: tabObj, @@ -112,11 +113,11 @@ const TabBody = ({ tabObj, index, tabFns, activeTab }) => { return ( { - console.log({ error }); - tabFns.close(error, tabObj.id); - tabFns.setActive('home'); - }} + // onError={(error, info) => { + // console.log({ error, info }); + // close.byIds([tabObj.id]); + // changeActiveTab('home'); + // }} > {_renderRequestTab(tabObj.type)} diff --git a/packages/firecamp-core/src/components/tabs/TabContainerBody.tsx b/packages/firecamp-core/src/components/tabs/TabContainerBody.tsx index 69c5af2c1..7f589db8a 100644 --- a/packages/firecamp-core/src/components/tabs/TabContainerBody.tsx +++ b/packages/firecamp-core/src/components/tabs/TabContainerBody.tsx @@ -1,9 +1,20 @@ import { Column } from '@firecamp/ui-kit'; import classnames from 'classnames'; +import shallow from 'zustand/shallow'; +import { ITabStore, useTabStore } from '../../store/tab'; import Home from './home/Home'; import TabBody from './TabBody'; -const TabContainerBody = ({ tabs = [], activeTab = '', tabFns = {} }) => { +const TabContainerBody = () => { + const { tabs, activeTab } = useTabStore( + (s: ITabStore) => ({ + tabs: s.list, + orders: s.orders, + activeTab: s.activeTab, + }), + shallow + ); + return (
{ >
- {tabs.map((t, i) => ( + {Object.values(tabs).map((t, i) => (
- +
))} diff --git a/packages/firecamp-core/src/components/tabs/TabContainerHeader.tsx b/packages/firecamp-core/src/components/tabs/TabContainerHeader.tsx index 7446cc4b9..441fb69ac 100644 --- a/packages/firecamp-core/src/components/tabs/TabContainerHeader.tsx +++ b/packages/firecamp-core/src/components/tabs/TabContainerHeader.tsx @@ -1,32 +1,49 @@ -import { FC, useMemo } from 'react'; +import { FC, useEffect, memo, useRef } from 'react'; import classnames from 'classnames'; -import { Column, Row, Tabs } from '@firecamp/ui-kit'; +import { Column, Row, TabsV3 as Tabs } from '@firecamp/ui-kit'; import { _misc } from '@firecamp/utils'; import { VscAdd } from '@react-icons/all-files/vsc/VscAdd'; import { VscHome } from '@react-icons/all-files/vsc/VscHome'; +import shallow from 'zustand/shallow'; -import PreComp from './header/PreComp'; -import { ITab, ITabFns } from './types/tab'; import Menu from './header/Menu'; import CollabButton from './header/CollabButton'; +import { ITabStore, useTabStore } from '../../store/tab'; -const TabHeaderContainer: FC = ({ - tabs = [], - activeTab = 'home', - tabFns, -}) => { - tabs = useMemo( - () => - tabs.map((t) => ({ - ...t, - name: t.name || t.request.meta.name, - preComp: () => ( - - ), - dotIndicator: t.meta?.hasChange === true, - })), - [tabs] +import { platformEmitter as emitter } from '../../services/platform-emitter'; +import { EPlatformTabs } from '../../services/platform-emitter/events'; +import { TId } from '@firecamp/types'; + +const TabHeaderContainer: FC = () => { + const tabApi = useRef(); + const { activeTab } = useTabStore( + (s: ITabStore) => ({ + activeTab: s.activeTab, + // orders: s.orders + }), + shallow ); + const { list: tabs, orders, changeActiveTab, changeOrders } = useTabStore.getState() as ITabStore; + + useEffect(() => { + console.log(tabApi, 'tabApi..'); + emitter.on(EPlatformTabs.opened, ([tab, orders]) => { + tabApi.current.add(tab); + }); + emitter.on(EPlatformTabs.closed, (tabId_s: TId | TId[]) => { + tabApi.current.close(tabId_s); + }); + return () => { + emitter.off(EPlatformTabs.opened); + emitter.off(EPlatformTabs.closed); + }; + }, []); + + const openNewTab = () => { + emitter.emit(EPlatformTabs.openNew); + }; + + console.log(tabs, orders, 'orders... 12'); return ( = ({ }, 'w-10 h-9 flex items-center justify-center cursor-pointer border-b bg-tabBackground2 text-tabForegroundInactive border-r border-tabBorder flex-none' )} - onClick={() => tabFns.setActive('home')} + onClick={() => changeActiveTab('home')} > - {/* {sortedTabs.length > 0 ? ( */} tabFns.close(null, id), + onClick: (id) => emitter.emit(EPlatformTabs.close, id), }} tabIndex={-1} focus={false} className="flex-1 overflow-x-auto overflow-y-visible visible-scrollbar request-tab-wrapper" - suffixComp={() => { - return ( -
-
tabFns.open()} - > - - - -
- + suffixComp={ +
+
openNewTab()} + > + + +
- ); - }} + +
+ } + reOrderable={true} + onReorder={changeOrders} /> - {/* ) : ( - '' - )} */}
@@ -94,15 +108,4 @@ const TabHeaderContainer: FC = ({ ); }; -export default TabHeaderContainer; - -interface ITabHeaderContainer { - tabs: Array; - - /** - * Current active tab - */ - activeTab?: string; - - tabFns: ITabFns; -} +export default memo(TabHeaderContainer); diff --git a/packages/firecamp-core/src/components/tabs/header/Menu.tsx b/packages/firecamp-core/src/components/tabs/header/Menu.tsx index b9a214ee7..6e171d894 100644 --- a/packages/firecamp-core/src/components/tabs/header/Menu.tsx +++ b/packages/firecamp-core/src/components/tabs/header/Menu.tsx @@ -10,12 +10,16 @@ import { FcSocketIoSquare, FcWebSocket, } from '../../common/icons'; -import { ITabFns } from '../types/tab'; + import getOs from '../../../services/get-os'; +import { platformEmitter as emitter } from '../../../services/platform-emitter'; +import { EPlatformTabs } from '../../../services/platform-emitter/events'; +import { useTabStore } from '../../../store/tab'; let osName = getOs(); -const Menu: FC = ({ tabFns }) => { +const Menu: FC = () => { + const { close } = useTabStore.getState(); const tabMenus = [ { header: 'Create A Request', @@ -29,7 +33,7 @@ const Menu: FC = ({ tabFns }) => { ), onClick: () => { - tabFns.open(ERequestTypes.Rest); + openNewTab(ERequestTypes.Rest); }, }, { @@ -41,7 +45,7 @@ const Menu: FC = ({ tabFns }) => { ), onClick: () => { - tabFns.open(ERequestTypes.GraphQL); + openNewTab(ERequestTypes.GraphQL); }, }, { @@ -53,7 +57,7 @@ const Menu: FC = ({ tabFns }) => { ), onClick: () => { - tabFns.open(ERequestTypes.WebSocket); + openNewTab(ERequestTypes.WebSocket); }, }, { @@ -65,7 +69,7 @@ const Menu: FC = ({ tabFns }) => { ), onClick: () => { - tabFns.open(ERequestTypes.SocketIO); + openNewTab(ERequestTypes.SocketIO); }, }, ], @@ -89,7 +93,7 @@ const Menu: FC = ({ tabFns }) => { ), onClick: (option, e) => { - tabFns.closeAll(e); + close.all(); }, }, { @@ -108,8 +112,7 @@ const Menu: FC = ({ tabFns }) => { ), onClick: (option, e) => { - // console.log({ option, e }); - tabFns.closeAllSaved(e); + close.allSaved(); }, }, /* { @@ -136,6 +139,10 @@ const Menu: FC = ({ tabFns }) => { }, ]; + const openNewTab = (type) => { + emitter.emit(EPlatformTabs.openNew, type); + }; + return ( @@ -150,7 +157,3 @@ const Menu: FC = ({ tabFns }) => { }; export default Menu; - -interface ITabMenu { - tabFns: ITabFns; -} diff --git a/packages/firecamp-core/src/components/tabs/header/TabHeaderContainer.tsx b/packages/firecamp-core/src/components/tabs/header/TabHeaderContainer.tsx deleted file mode 100644 index dab800d55..000000000 --- a/packages/firecamp-core/src/components/tabs/header/TabHeaderContainer.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { FC, useMemo } from 'react'; -import classnames from 'classnames'; -import { Column, Row, Tabs } from '@firecamp/ui-kit'; -import { _misc } from '@firecamp/utils'; -import { VscAdd } from '@react-icons/all-files/vsc/VscAdd'; -import { VscHome } from '@react-icons/all-files/vsc/VscHome'; - -import PreComp from './PreComp'; -import { ITab, ITabFns } from '../types/tab'; -import Menu from './Menu'; -import CollabButton from './CollabButton'; - -const TabHeaderContainer: FC = ({ - tabs = [], - activeTab = 'home', - _tabFns, -}) => { - tabs = useMemo( - () => - tabs.map((t) => ({ - ...t, - name: t.name || t.request.meta.name, - preComp: () => ( - - ), - dotIndicator: t.meta?.hasChange === true, - })), - [tabs] - ); - - return ( - - - -
-
-
_tabFns.setActive('home')} - > - - - -
- {/* {sortedTabs.length > 0 ? ( */} - _tabFns.close(null, id), - }} - focus={false} - className="flex-1 overflow-x-auto overflow-y-visible visible-scrollbar request-tab-wrapper" - suffixComp={() => { - return ( -
-
_tabFns.open()} - > - - - -
- -
- ); - }} - /> - {/* ) : ( - '' - )} */} -
-
-
- -
-
- ); -}; - -export default TabHeaderContainer; - -interface ITabHeaderContainer { - tabs: Array; - - /** - * Current active tab - */ - activeTab?: string; - - _tabFns: ITabFns; -} diff --git a/packages/firecamp-core/src/components/tabs/home/Home.tsx b/packages/firecamp-core/src/components/tabs/home/Home.tsx index 7e4b95976..5fde490f0 100644 --- a/packages/firecamp-core/src/components/tabs/home/Home.tsx +++ b/packages/firecamp-core/src/components/tabs/home/Home.tsx @@ -17,32 +17,28 @@ import { FcWebSocket, } from '../../common/icons'; -import { useTabStore } from '../../../store/tab'; import { usePlatformStore } from '../../../store/platform'; import { EThemeColor, EThemeMode } from '../../../types'; +import { platformEmitter as emitter } from '../../../services/platform-emitter' +import { EPlatformTabs } from '../../../services/platform-emitter/events' const Home: FC = () => { - const tabsStore = useTabStore.getState(); useEffect(() => { // F?.reactGA?.pageview?.('home'); }, []); const _openTab = ( - type?: ERequestTypes, - subType?: string, - early_access?: boolean + type?: ERequestTypes ) => { - let allowed_app = [ + const allowed_app = [ ERequestTypes.SocketIO, ERequestTypes.WebSocket, ERequestTypes.Rest, ERequestTypes.GraphQL, ]; - if (!allowed_app.includes(type) && !subType) return; //todo: release hack here for SocketIO beta release, only open socket tab - - // console.log('type', type); - tabsStore.open.new(type, true, subType); + // if (!allowed_app.includes(type)) + emitter.emit(EPlatformTabs.openNew, type); }; const apiCategories = [ diff --git a/packages/firecamp-core/src/components/tabs/types/tab.ts b/packages/firecamp-core/src/components/tabs/types/tab.ts index 13ebd7113..3314753ec 100644 --- a/packages/firecamp-core/src/components/tabs/types/tab.ts +++ b/packages/firecamp-core/src/components/tabs/types/tab.ts @@ -1,3 +1,4 @@ +import { ReactNode } from 'react'; import { EFirecampAgent, EHttpMethod, @@ -6,40 +7,21 @@ import { I_Meta, TId, } from '@firecamp/types'; +import { ITab } from '@firecamp/ui-kit'; import { IPlatformRequestService, IPlatformEnvironmentService, } from '../../../services/platform-context'; -/** - * Firecamp request tab - */ -export interface ITab { - /** - * Unique identification for tab - */ - id: string; - - /** - * Request tab name - */ - name: string; - - /** - * Request type - */ +/** Firecamp request tab */ +export interface IRequestTab extends ITab { + /** request type */ type: string; - /** - * Request sub-type - */ - subType?: string; - - /** - * Request tab meta - */ - meta?: ITabMeta; + /** request meta */ + meta?: IRequestTabMeta; + /** minimal request */ request?: { url?: IUrl; method?: EHttpMethod; @@ -51,7 +33,7 @@ export interface ITab { /** * Tab meta */ -export interface ITabMeta { +export interface IRequestTabMeta { /** * Whether request tab is saved or not */ @@ -87,54 +69,15 @@ export interface ITabMeta { * Tab functions to perform actions */ -export interface ITabFns { - //set active tab - setActive: (tabId: string) => void; - - // open a new tab - open: (tabType?: string, subType?: string) => void; - - // close tab - close: (e: any, tabId: string, doSave?: boolean) => void; - - // reorder tabs on drag and drop sequence - reorder: (dragIndex: number, hoverIndex: number) => void; - - // close all tabs - closeAll: (e?: any) => void; - - // close all saved tabs - closeAllSaved: (e?: Event) => void; - - // close all fresh tabs - closeAllFresh: (e?: any) => void; - - // close all tabs except active one - closeAllExceptActive: (e?: any) => void; - - // save active tab - save: (e?: any) => void; - - fetchRequest?: (reqId: string) => any; - - mount?: (clientTabData: object) => void; - - // update tab meta - updateMeta?: (tabId: TId, meta: ITabMeta) => void; - - // update tab root keys - updateRootKeys: (tabId: TId, updatedTab: Partial) => void; -} - -export interface ITabProps { +export interface IRequestTabProps { index: number; - tab: ITab; + tab: IRequestTab; activeTab?: TId; //v3 props platformComponents: { - SavePopover?: JSX.Element; - EnvironmentWidget: JSX.Element; + SavePopover?: ReactNode; + EnvironmentWidget: ReactNode; }; envVariables?: { mergedEnvVariables: object; diff --git a/packages/firecamp-core/src/containers/App.tsx b/packages/firecamp-core/src/containers/App.tsx index a781a57c3..9c8383d1c 100644 --- a/packages/firecamp-core/src/containers/App.tsx +++ b/packages/firecamp-core/src/containers/App.tsx @@ -9,8 +9,8 @@ import '../sass/_index.sass'; import { FC, useEffect } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import shallow from 'zustand/shallow'; -// import { HTML5Backend } from 'react-dnd-html5-backend'; -// import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { DndProvider } from 'react-dnd'; import { Row, RootContainer } from '@firecamp/ui-kit'; @@ -28,7 +28,6 @@ import { ModalContainer } from '../components/modals-v3/ModalContainer'; import { EnvSidebarContainer } from '../components/sidebar'; import ErrorPopup from '../components/common/error-boundary/ErrorPopup'; -import useMonacoWorkers from '../components/hooks/useMonacoWorkers'; import AppService from '../services/app'; import RealtimeEventManager from '../components/common/realtime/Realtime' @@ -65,14 +64,12 @@ const App: FC = () => { return ( { - return ; - }} + FallbackComponent={ErrorPopup} onError={(error) => { console.log({ error }); }} > - {/* */} + = () => { - {/* */} + diff --git a/packages/firecamp-core/src/services/app.ts b/packages/firecamp-core/src/services/app.ts index b0a73eafa..1128718c3 100644 --- a/packages/firecamp-core/src/services/app.ts +++ b/packages/firecamp-core/src/services/app.ts @@ -4,13 +4,14 @@ import notification from './notification'; import modalService from './modals'; import { useUserStore } from '../store/user'; import { useWorkspaceStore } from '../store/workspace'; -import { ECloudApiHeaders, prepareEventNameForRequestPull } from '../types'; +import { ECloudApiHeaders } from '../types'; import { IOrganization, IWorkspace } from '@firecamp/types'; import { usePlatformStore } from '../store/platform'; import { useTabStore } from '../store/tab'; import { useEnvStore } from '../store/environment'; import { useModalStore } from '../store/modal'; import { platformEmitter } from './platform-emitter'; +import { prepareEventNameForRequestPull } from './platform-emitter/events'; const userService = { isLoggedIn: () => { diff --git a/packages/firecamp-core/src/services/platform-context/environment.ts b/packages/firecamp-core/src/services/platform-context/environment.ts index 3a44c6ffb..fa5705dbc 100644 --- a/packages/firecamp-core/src/services/platform-context/environment.ts +++ b/packages/firecamp-core/src/services/platform-context/environment.ts @@ -8,10 +8,9 @@ import { _object } from '@firecamp/utils'; import { useTabStore } from '../../store/tab'; import { IEnvironmentStore, useEnvStore } from '../../store/environment'; -import { ITab } from '../../components/tabs/types/tab'; -import { platformEmitter } from '../platform-emitter'; -import { prepareEventNameForEnvToTab } from '../../types'; -// import {ITab} +import { IRequestTab } from '../../components/tabs/types/tab'; +import { prepareEventNameForEnvToTab } from '../platform-emitter/events' +import { platformEmitter } from '../platform-emitter' interface IPlatformEnvironmentService { // subscribe to environment changes @@ -108,7 +107,7 @@ const environment: IPlatformEnvironmentService = { getActiveEnvsByTabId: (tabId: TId) => { const envStore: IEnvironmentStore = useEnvStore.getState(); - let tab: ITab = useTabStore.getState().list.find((t) => t.id === tabId); + let tab: IRequestTab = useTabStore.getState().list[tabId]; if (!tab || !tabId) return Promise.reject('invalid tab id'); //workspace active environment @@ -130,7 +129,7 @@ const environment: IPlatformEnvironmentService = { // get variables by tab id getVariablesByTabId: async (tabId: TId) => { const envStore: IEnvironmentStore = useEnvStore.getState(); - const tab: ITab = useTabStore.getState().list.find((t) => t.id === tabId); + const tab: IRequestTab = useTabStore.getState().list[tabId]; if (!tab || !tabId) return Promise.reject('invalid tab id'); const activeEnvsOfTab = await environment.getActiveEnvsByTabId(tabId); diff --git a/packages/firecamp-core/src/services/platform-context/request.ts b/packages/firecamp-core/src/services/platform-context/request.ts index 6697cfbba..49d781b70 100644 --- a/packages/firecamp-core/src/services/platform-context/request.ts +++ b/packages/firecamp-core/src/services/platform-context/request.ts @@ -17,11 +17,11 @@ import { useRequestStore } from '../../store/request'; import { useUserStore } from '../../store/user'; import { usePlatformStore } from '../../store/platform'; -import { ITabMeta } from '../../components/tabs/types'; +import { IRequestTab } from '../../components/tabs/types'; import { platformEmitter } from '../platform-emitter'; import AppService from '../app'; -import { prepareEventNameForRequestPull } from '../../types'; +import { prepareEventNameForRequestPull } from '../platform-emitter/events'; interface IPlatformRequestService { // subscribe real-time request changes (pull-actions from server) @@ -36,7 +36,7 @@ interface IPlatformRequestService { // on change request, update tab meta onChangeRequestTab: ( tabId: TId, - tabMeta: ITabMeta, + tabMeta: IRequestTab['meta'], request?: IRest | IGraphQL, // |ISocket | IWebsocket , pushActions?: any[] ) => void; @@ -167,14 +167,14 @@ const request: IPlatformRequestService = { // on change request onChangeRequestTab: ( tabId: TId, - tabMeta: ITabMeta, + tabMeta: IRequestTab['meta'], request?: IRest | IGraphQL, // | ISocket | IWebsocket, pushActions?: any[] ) => { // Here, request and pushActions are used for future purpose // console.log({ tabMeta }); - useTabStore.getState().update.meta(tabId, tabMeta); + useTabStore.getState().changeMeta(tabId, tabMeta); }, // execute request diff --git a/packages/firecamp-core/src/services/platform-emitter/events.ts b/packages/firecamp-core/src/services/platform-emitter/events.ts new file mode 100644 index 000000000..a54717636 --- /dev/null +++ b/packages/firecamp-core/src/services/platform-emitter/events.ts @@ -0,0 +1,46 @@ +import { TId } from '@firecamp/types'; + +export enum PlatformEvents { + WorkspaceEvents = 'platform/workspace.events', + ExplorerEvents = 'platform/explorer.events', + EnvironmentEvents = 'platform/environment.events', + TabEvents = 'platform/tabs.events', +} +export enum PlatformWorkspaceEvents { + WorkspaceCreated = 'platform/workspace.created', + WorkspaceUpdated = 'platform/workspace.updated', + WorkspaceDeleted = 'platform/workspace.deleted', +} + +export enum PlatformExplorerEvents { + CollectionCreated = 'platform/collection.created', + CollectionUpdated = 'platform/collection.updated', + CollectionDeleted = 'platform/collection.deleted', + + FolderCreated = 'platform/folder.created', + FolderUpdated = 'platform/folder.updated', + FolderDeleted = 'platform/folder.deleted', + + RequestCreated = 'platform/request.created', + RequestUpdated = 'platform/request.updated', + RequestDeleted = 'platform/request.deleted', +} + +export enum PlatformEnvironmentEvents { + EnvironmentCreated = 'platform/env.created', + EnvironmentUpdated = 'platform/env.updated', + EnvironmentDeleted = 'platform/env.deleted', +} + +export enum EPlatformTabs { + openNew = 'platform/tabs.openNew', + openRequest = 'platform/tabs.openRequest', + openSaved = 'platform/tabs.openSaved', + close = 'platform/tabs.close', + + opened = 'platform/tabs.opened', + closed = 'platform/tabs.closed', +} + +export const prepareEventNameForRequestPull = (reqId: TId) => `pull/r/${reqId}`; +export const prepareEventNameForEnvToTab = (tabId: TId) => `env/t/${tabId}`; diff --git a/packages/firecamp-core/src/store/request.ts b/packages/firecamp-core/src/store/request.ts index 3d61c7376..7993558fc 100644 --- a/packages/firecamp-core/src/store/request.ts +++ b/packages/firecamp-core/src/store/request.ts @@ -88,7 +88,7 @@ export const useRequestStore = create((set, get) => ({ requestBeingSaved && requestBeingSaved._action.type === EPushActionType.Insert ) { - tabState.update.rootKeys(requestTabId, { + tabState.changeRootKeys(requestTabId, { name: requestBeingSaved?.meta?.name || 'Untitled request', type: requestBeingSaved?.meta?.type || '', // subType: requestBeingSaved?.meta?.data_type || '', @@ -108,7 +108,7 @@ export const useRequestStore = create((set, get) => ({ }); } else { // update tab meta on save request - tabState.update.meta(requestTabId, { + tabState.changeMeta(requestTabId, { isSaved: true, hasChange: false, isFresh: false, diff --git a/packages/firecamp-core/src/store/tab.ts b/packages/firecamp-core/src/store/tab.ts index d17131dae..f664ce575 100644 --- a/packages/firecamp-core/src/store/tab.ts +++ b/packages/firecamp-core/src/store/tab.ts @@ -1,31 +1,34 @@ import create from 'zustand'; import _reject from 'lodash/reject'; import { nanoid } from 'nanoid'; -import { TId } from '@firecamp/types'; +import { ERequestTypes, TId } from '@firecamp/types'; import { _object } from '@firecamp/utils'; +import { dissoc } from 'ramda'; -import { ITab, ITabMeta } from '../components/tabs/types'; +import { IRequestTab } from '../components/tabs/types'; const initialState = { - list: [], + list: {}, activeTab: 'home', orders: [], }; interface ITabStore { - list: any[]; - activeTab: string; - orders: string[]; + list: Record; + activeTab: TId; + orders: TId[]; reorder: (dragIndex: number, hoverIndex: number) => void; remove: (tbId: string) => void; - update: { - meta: (tab, meta, request?: any) => void; //todo: define types... - activeTab: (tabId: string) => void; - rootKeys: (tabId: TId, updatedTab: Partial) => void; - }; + changeMeta: (tab, meta, request?: any) => void; //todo: define types... + changeActiveTab: (tabId: string) => void; + changeRootKeys: (tabId: TId, updatedTab: Partial) => void; + changeOrders: (orders: TId[]) => void; open: { - new: (type: string, isActive: boolean, subType: string) => void; + new: ( + type?: string, + isActive?: boolean + ) => [tab: IRequestTab, orders: TId[]]; request: ( request: any, options: { @@ -34,8 +37,8 @@ interface ITabStore { isHistoryTab?: boolean; _meta?: any; } - ) => void; - saved: (request: any) => void; + ) => [tab: IRequestTab, orders: TId[]]; + saved: (request: any) => [tab: IRequestTab, orders: TId[]]; }; close: { all: () => void; @@ -82,74 +85,90 @@ const useTabStore = create((set, get) => { remove: (tabId) => { set((s) => { - // s.update.meta(tabId, { isClosed: true }); - let index = s.list.findIndex((t) => t.id == tabId); + // s.changeMeta(tabId, { isClosed: true }); + const index = s.orders.findIndex((t) => t == tabId); if (index == -1) return s; - let activeTab = + const activeTab = tabId == s.activeTab ? index == 0 ? 'home' - : s.list[index - 1].id + : s.orders[index - 1] : s.activeTab; /*To remove tab from cacheTabs*/ // cacheTabsFactoryFns.removeTab(tabId) - let list = [..._reject(s.list, (t) => t.id == tabId)]; + const list = dissoc(tabId, s.list); return { list, activeTab, - orders: list.map((t) => t.id), + orders: s.orders.filter((id) => id != tabId), }; }); }, - update: { - meta: (tabId: TId, meta: ITabMeta) => { - set((s) => { - let tabs = s.list.map((t) => { - if (t.id !== tabId) return t; - // console.log({ meta, tabId }); - - return { - ...t, - meta: { ...t.meta, ...meta }, - // request: request || t.request, - }; - }); - return { list: [...tabs] }; - }); - }, + changeMeta: (tabId: TId, meta: IRequestTab['meta']) => { + set((s) => { + const tab = s.list[tabId]; + const list = { + ...s.list, + [tabId]: { + ...tab, + meta: { + ...tab.meta, + ...meta, + }, + }, + }; + return { list }; + }); + }, - activeTab: (tabId) => { - set((s) => ({ activeTab: tabId })); - // tab.storeCacheTabsInDBWithDebounce(); - }, + changeActiveTab: (tabId) => { + set((s) => ({ activeTab: tabId })); + // tab.storeCacheTabsInDBWithDebounce(); + }, - rootKeys: (tabId: TId, updatedTab: Partial) => { - set((s) => { - let tabs = s.list.map((t) => { - if (t.id !== tabId) return t; - // console.log({ meta, tabId }); + changeRootKeys: (tabId: TId, updatedTab: Partial) => { + set((s) => { + const list = { + ...s.list, + [tabId]: { + ...s.list[tabId], + ...updatedTab, + }, + }; + return { list }; + }); + }, - return { - ...t, - ...updatedTab, - }; - }); - return { list: [...tabs] }; - }); - }, + changeOrders: (orders) => { + set((s) => ({ + orders, + })); }, open: { - new: (type: string, isActive: boolean, subType: string = '') => { - let tab: ITab = { - id: nanoid(), + new: (type, isActive) => { + const { list, orders, activeTab } = get(); + const tId = nanoid(); + if (!type) { + if (orders.length === 0) type = ERequestTypes.Rest; + else { + let tab; + if (activeTab === 'home') { + tab = list[orders[orders.length - 1]]; + } else { + tab = list[activeTab]; + } + type = tab?.type; + } + } + const tab: IRequestTab = { + id: tId, name: 'New Tab', - type, - subType, + type: type || ERequestTypes.Rest, meta: { isSaved: false, hasChange: false, @@ -161,28 +180,27 @@ const useTabStore = create((set, get) => { /*To add tab in cacheTabs*/ // cacheTabsFactoryFns.setTab(tab.id, cacheTabPayload) - let state = get(); - let list = [...state.list, tab]; + const _list = { ...list, [tId]: tab }; - set((s) => { - return { - list, - activeTab: isActive == true ? tab.id : s.activeTab, - orders: list.map((t) => t.id), - }; - }); + const _orders = [...orders, tId]; + set((s) => ({ + list: _list, + activeTab: isActive == true ? tab.id : s.activeTab, + orders: _orders, + })); + return [tab, _orders]; }, request: ( request, { setActive = false, isSaved = true, isHistoryTab = false, _meta = {} } ) => { - let { list, activeTab } = get(); - let tab = { - id: nanoid(), + const { list, orders, activeTab } = get(); + const tId = nanoid(); + const tab: IRequestTab = { + id: tId, name: request?.meta?.name || 'untitled request', type: request.meta.type, - subType: request.meta ? request.meta.data_type : '', request, meta: { isSaved: isSaved, @@ -191,23 +209,26 @@ const useTabStore = create((set, get) => { revision: 1, isDeleted: false, isHistoryTab, - _meta, + // _meta, }, }; /*To add tab in cacheTabs*/ // cacheTabsFactoryFns.setTab(tab.id, cacheTabPayload) - set((s: any) => { + const _orders = [...orders, tId]; + set((s: ITabStore) => { return { - list: [...list, tab], + list: { ...list, [tId]: tab }, activeTab: setActive == true ? tab.id : activeTab, - orders: list.map((t) => t.id), + orders: _orders, }; }); + + return [tab, _orders]; }, - saved: async ({ name, url, method, meta, _meta }) => { + saved: ({ name, url, method, meta, _meta }) => { // Todo: need to improve this old structure // note: above request is coming from explorer/tree item @@ -218,16 +239,16 @@ const useTabStore = create((set, get) => { _meta, }; - let { list, update, open } = get(); - let tabAlreadyExists = list.find( + let { list, changeActiveTab, open } = get(); + let tabAlreadyExists = Object.values(list).find( (l) => l?.request?._meta?.id == request?._meta?.id ); // console.log(tabAlreadyExists); if (tabAlreadyExists) { - update.activeTab(tabAlreadyExists.id); - return; + changeActiveTab(tabAlreadyExists.id); + return null; } // console.log('in store...', request); @@ -244,7 +265,7 @@ const useTabStore = create((set, get) => { all: () => { // cacheTabsFactoryFns.closeAllTabs(); set((s) => ({ - list: [], + list: {}, activeTab: 'home', orders: [], })); @@ -267,30 +288,29 @@ const useTabStore = create((set, get) => { } }, - allLeft: async (id = '') => { - let { list, activeTab, close } = get(); + allLeft: async (tabId: TId) => { + let { orders, activeTab, close } = get(); let tabsToClose = []; let tabIndex = -1; - if (id?.length) { - tabIndex = list.findIndex((tab) => tab.id === id); + if (tabId) { + tabIndex = orders.findIndex((id) => tabId === id); } else { - tabIndex = list.findIndex((tab) => tab.id === activeTab); + tabIndex = orders.findIndex((id) => id === activeTab); } if (tabIndex > 0) { - tabsToClose = list.slice(0, tabIndex); + tabsToClose = orders.slice(0, tabIndex); } if (tabsToClose) { console.log(`Close left: tabsToClose`, tabsToClose); - let ids = tabsToClose.map((tab) => tab.id); - close.byIds(ids); + close.byIds(tabsToClose); } }, - allRight: async (id = '') => { - let { list, activeTab, close } = get(); + allRight: async (tabId: TId) => { + let { orders, activeTab, close } = get(); let tabsToClose = []; let tabIndex = -1; @@ -300,50 +320,47 @@ const useTabStore = create((set, get) => { return; } - if (id?.length) { - tabIndex = list.findIndex((tab) => tab.id === id); + if (tabId) { + tabIndex = orders.findIndex((id) => tabId === id); } else { - tabIndex = list.findIndex((tab) => tab.id === activeTab); + tabIndex = orders.findIndex((id) => id === activeTab); } - if (tabIndex > list.length && tabIndex !== -1) { - tabsToClose = list.slice(tabIndex + 1); + if (tabIndex > orders.length && tabIndex !== -1) { + tabsToClose = orders.slice(tabIndex + 1); } if (tabsToClose) { console.log(`Close right: tabsToClose`, tabsToClose); - - let ids = tabsToClose.map((tab) => tab.id); - close.byIds(ids); + close.byIds(tabsToClose); } }, allExceptActive: async () => { - let { list, activeTab, close } = get(); + let { orders, activeTab, close } = get(); let tabsToClose = []; if (activeTab === 'home') { close.all(); return; } - let tabIndex = list.findIndex((tab) => tab.id === activeTab); - if (tabIndex <= list.length && tabIndex !== -1) { + let tabIndex = orders.findIndex((id) => id === activeTab); + if (tabIndex <= orders.length && tabIndex !== -1) { tabsToClose = [ - ...list.slice(0, tabIndex), - ...list.slice(tabIndex + 1), + ...orders.slice(0, tabIndex), + ...orders.slice(tabIndex + 1), ]; } if (tabsToClose) { console.log(`Close except active: tabsToClose`, tabsToClose); - let ids = tabsToClose.map((tab) => tab.id); - close.byIds(ids); + close.byIds(tabsToClose); } }, allSaved: async () => { - let { list, activeTab, close } = get(); + let { list, close } = get(); - let tabsToClose = list.filter( + let tabsToClose = Object.values(list).filter( (tab) => tab?.meta?.isSaved === true && tab.meta?.hasChange !== true ); // console.log({ list, tabsToClose }); @@ -357,7 +374,7 @@ const useTabStore = create((set, get) => { allFresh: async () => { let { list, close } = get(); - let tabsToClose = list.filter( + let tabsToClose = Object.values(list).filter( (tab) => tab.meta && tab.meta.isSaved === false && @@ -373,7 +390,7 @@ const useTabStore = create((set, get) => { allDirty: async () => { let { list, close } = get(); - let tabsToClose = list.filter( + let tabsToClose = Object.values(list).filter( (tab) => tab.meta && tab.meta.isSaved === true && tab.meta.hasChange === true ); diff --git a/packages/firecamp-core/src/types.ts b/packages/firecamp-core/src/types.ts index c7edef990..2a2b0f989 100644 --- a/packages/firecamp-core/src/types.ts +++ b/packages/firecamp-core/src/types.ts @@ -1,5 +1,3 @@ -import { TId } from '@firecamp/types'; - export enum EWorkspaceTypes { Personal = 1, Organizational = 2, @@ -163,37 +161,3 @@ export const DefaultTheme = { mode: EThemeMode.Light, color: EThemeColor.Orange, }; - -export enum PlatformEvents { - WorkspaceEvents = 'platform/workspace.events', - ExplorerEvents = 'platform/explorer.events', - EnvironmentEvents = 'platform/environment.events', -} -export enum PlatformWorkspaceEvents { - WorkspaceCreated = 'platform/workspace.created', - WorkspaceUpdated = 'platform/workspace.updated', - WorkspaceDeleted = 'platform/workspace.deleted', -} - -export enum PlatformExplorerEvents { - CollectionCreated = 'platform/collection.created', - CollectionUpdated = 'platform/collection.updated', - CollectionDeleted = 'platform/collection.deleted', - - FolderCreated = 'platform/folder.created', - FolderUpdated = 'platform/folder.updated', - FolderDeleted = 'platform/folder.deleted', - - RequestCreated = 'platform/request.created', - RequestUpdated = 'platform/request.updated', - RequestDeleted = 'platform/request.deleted', -} - -export enum PlatformEnvironmentEvents { - EnvironmentCreated = 'platform/environment.created', - EnvironmentUpdated = 'platform/environment.updated', - EnvironmentDeleted = 'platform/environment.deleted', -} - -export const prepareEventNameForRequestPull = (reqId: TId) => `pull/r/${reqId}`; -export const prepareEventNameForEnvToTab = (tabId: TId) => `env/t/${tabId}`; diff --git a/packages/firecamp-rest/src/components/common/code-snippets/CodeSnippets.tsx b/packages/firecamp-rest/src/components/common/code-snippets/CodeSnippets.tsx index 07694fe3e..6d292c932 100644 --- a/packages/firecamp-rest/src/components/common/code-snippets/CodeSnippets.tsx +++ b/packages/firecamp-rest/src/components/common/code-snippets/CodeSnippets.tsx @@ -1,6 +1,7 @@ import { useEffect, useState, useMemo } from 'react'; import classnames from 'classnames'; import { Editor, Container, Tabs, Modal } from '@firecamp/ui-kit'; +import { _env } from '@firecamp/utils'; import shallow from 'zustand/shallow'; import codeSnippet, { @@ -9,15 +10,14 @@ import codeSnippet, { } from '../../../services/code-snippet'; import targetsInfo from '../../../services/code-snippet/targets-info'; import { useRestStoreApi, useRestStore, IRestStore } from '../../../store'; -// import Modal from 'react-responsive-modal'; -import { _env } from '@firecamp/utils'; const CodeSnippets = ({ tabId = '', getPlatformEnvironments }) => { - let restStoreApi: any = useRestStoreApi(); - let { isCodeSnippetOpen, toggleOpenCodeSnippet } = useRestStore( + let { request, toggleOpenCodeSnippet } = + useRestStoreApi().getState() as IRestStore; + const envVariables = getPlatformEnvironments(tabId); + const { isCodeSnippetOpen } = useRestStore( (s: IRestStore) => ({ isCodeSnippetOpen: s.ui.isCodeSnippetOpen, - toggleOpenCodeSnippet: s.toggleOpenCodeSnippet, }), shallow ); @@ -27,39 +27,35 @@ const CodeSnippets = ({ tabId = '', getPlatformEnvironments }) => { * Reason: To have an active client (persist) for individual target * Example: {'javascript': 'axios', 'java': 'okhttp', 'c': 'libcurl', ...} */ - let [activeClientTargetMap, setActiveClientTargetMap] = useState({}); - let [activeTarget, setActiveTarget] = useState(''); + const [activeClientTargetMap, setActiveClientTargetMap] = useState({}); + const [activeTarget, setActiveTarget] = useState(''); // Snippets and generated tabs - let [tabs] = useMemo(() => { - tabs = targetsInfo.map((t) => { + let tabs = useMemo(() => { + return targetsInfo.map((t) => { return { id: t.target, name: t.target, }; }); - return [tabs]; }, [isCodeSnippetOpen]); // active snippet target payload - let activeSnippetTab = useMemo(() => { + const activeSnippetTab = useMemo(() => { return targetsInfo.find((s) => s.target === activeTarget); }, [activeTarget]); useEffect(() => { - let initTarget: ESnippetTargets = targetsInfo[0].target as ESnippetTargets, + const initTarget: ESnippetTargets = targetsInfo[0] + .target as ESnippetTargets, initClient: TTargetClients = targetsInfo[0].clients[0] as TTargetClients; _onSelectTab(initTarget, initClient); }, []); - let [snippetCode, setSnippetCode] = useState(''); + const [snippetCode, setSnippetCode] = useState(''); - /** - * Set active tab/ target and client - * @param tab : active tab/ target - * @param client : active client - */ - let _onSelectTab = async (tab: ESnippetTargets, client?: TTargetClients) => { + /** set active tab/ target and client*/ + const _onSelectTab = async (tab: ESnippetTargets, client?: TTargetClients) => { // set active tab if (tab !== activeTarget) { setActiveTarget(tab); @@ -81,9 +77,6 @@ const CodeSnippets = ({ tabId = '', getPlatformEnvironments }) => { } } - let request = restStoreApi?.getState().request; - let envVariables = await getPlatformEnvironments(tabId); - // Parse variables request = _env.applyVariables(request, envVariables.mergedEnvVariables); @@ -122,7 +115,7 @@ const CodeSnippets = ({ tabId = '', getPlatformEnvironments }) => {
-
+
{activeSnippetTab?.clients ? (
diff --git a/packages/ui-kit/.storybook/main.js b/packages/ui-kit/.storybook/main.js index 8449ba389..cd494d695 100644 --- a/packages/ui-kit/.storybook/main.js +++ b/packages/ui-kit/.storybook/main.js @@ -41,6 +41,7 @@ module.exports = { // config.resolve.alias = { // ...config.resolve.alias, + // "https": path.join(__dirname, "../../../node_modules/@types/node/https") // "react/jsx-dev-runtime": path.join(__dirname, "../../../node_modules/react/jsx-dev-runtime.js"), // "react/jsx-runtime": path.join(__dirname, "../../../node_modules/react/jsx-runtime.js") // } diff --git a/packages/ui-kit/.storybook/preview.js b/packages/ui-kit/.storybook/preview.js index 93695b116..5edad86cf 100644 --- a/packages/ui-kit/.storybook/preview.js +++ b/packages/ui-kit/.storybook/preview.js @@ -1,9 +1,11 @@ //to access tailwind.scss styles everywhere in project import '../src/scss/tailwind.scss'; +import { useState, useEffect } from "react"; import { withTests } from '@storybook/addon-jest'; +import { addDecorator } from "@storybook/react"; +import cx from 'classnames'; import results from '../.jest-test-results.json'; - export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, controls: { @@ -18,3 +20,117 @@ export const decorators = [ results, }), ]; + +//adding globally theme-selection +addDecorator((story) => { + return
+ + {story()} +
+}) + + +const StoryTheme = ({initialClassName = ""}) => { +const STORY_THEME = { + mode: { + Light: 'light', + Dark: 'dark', + }, + color: { + Green : 'green', + Orange : 'orange', + } +} + + const themes = [ + { + className: 'themeBG light-green', + value: { + name: 'theme-light primary-green', + class: 'theme-light primary-green', + mode: STORY_THEME.mode.Light, + color: STORY_THEME.color.Green, + }, + }, + { + className: 'themeBG light-orange', + value: { + name: 'theme-light primary-orange', + class: 'theme-light primary-orange', + mode: STORY_THEME.mode.Light, + color: STORY_THEME.color.Orange, + }, + }, + { + className: 'themeBG dark-green', + value: { + name: 'theme-dark primary-green', + class: 'theme-dark primary-green', + mode: STORY_THEME.mode.Dark, + color: STORY_THEME.color.Green, + }, + }, + { + className: 'themeBG dark-orange', + value: { + name: 'theme-dark primary-orange', + class: 'theme-dark primary-orange', + mode: STORY_THEME.mode.Dark, + color: STORY_THEME.color.Orange, + }, + }, + ]; + + const [theme, updateTheme] = useState(themes[0].value) + + + useEffect(() => { + try { + // Set app body theme + document.body.className = `${initialClassName} theme-${theme?.mode || 'light'} primary-${ + theme?.color || 'orange' + }`; + } catch (error) { + console.log({ error }); + } + }, [theme || {}]); + + const _setTheme = (theme) => { + try { + updateTheme(theme); + } catch (error) { + console.error(error); + } + }; + + return ( +
+
+ Themes +
+
+ {themes.map((th, index) => { + return ( +
{ + _setTheme(th.value); + }} + > +
+
+ ); + })} +
+
+ ); + +} \ No newline at end of file diff --git a/packages/ui-kit/__mocks__/ResizeObserver.ts b/packages/ui-kit/__mocks__/ResizeObserver.ts new file mode 100644 index 000000000..fe66b5e93 --- /dev/null +++ b/packages/ui-kit/__mocks__/ResizeObserver.ts @@ -0,0 +1,9 @@ +const ResizeObserver = + window.ResizeObserver || + jest.fn().mockImplementation(() => ({ + disconnect: jest.fn(), + observe: jest.fn(), + unobserve: jest.fn(), + })); +export default ResizeObserver; + diff --git a/packages/ui-kit/__mocks__/eventMock.ts b/packages/ui-kit/__mocks__/eventMock.ts new file mode 100644 index 000000000..90a5cd342 --- /dev/null +++ b/packages/ui-kit/__mocks__/eventMock.ts @@ -0,0 +1,62 @@ +import {fireEvent} from "@testing-library/react"; + +/* Drag & drop event */ +const dragAndDrop = async (elemDrag: HTMLElement, elemDrop: HTMLElement) => { + // calculate positions + let pos = elemDrag.getBoundingClientRect(); + const center1X = Math.floor((pos.left + pos.right) / 2); + const center1Y = Math.floor((pos.top + pos.bottom) / 2); + + pos = elemDrop.getBoundingClientRect(); + const center2X = Math.floor((pos.left + pos.right) / 2); + const center2Y = Math.floor((pos.top + pos.bottom) / 2); + + // mouse over dragged element and mousedown + await fireEvent.mouseMove(elemDrag, {clientX: center1X, clientY: center1Y}); + await fireEvent.mouseEnter(elemDrag, {clientX: center1X, clientY: center1Y}); + await fireEvent.mouseOver(elemDrag, {clientX: center1X, clientY: center1Y}); + await fireEvent.mouseDown(elemDrag, {clientX: center1X, clientY: center1Y}); + + // start dragging process over to drop target + const dragStarted = await fireEvent.dragStart(elemDrag, {clientX: center1X, clientY: center1Y}); + if (!dragStarted) { + return; + } + + await fireEvent.drag(elemDrag, {clientX: center1X, clientY: center1Y}); + await fireEvent.mouseMove(elemDrag, {clientX: center1X, clientY: center1Y}); + await fireEvent.drag(elemDrag, {clientX: center2X, clientY: center2Y}); + await fireEvent.mouseMove(elemDrop, {clientX: center2X, clientY: center2Y}); + + // trigger dragging process on top of drop target + await fireEvent.mouseEnter(elemDrop, {clientX: center2X, clientY: center2Y}); + await fireEvent.dragEnter(elemDrop, {clientX: center2X, clientY: center2Y}); + await fireEvent.mouseOver(elemDrop, {clientX: center2X, clientY: center2Y}); + await fireEvent.dragOver(elemDrop, {clientX: center2X, clientY: center2Y}); + + // release dragged element on top of drop target + await fireEvent.drop(elemDrop, {clientX: center2X, clientY: center2Y}); + await fireEvent.dragEnd(elemDrag, {clientX: center2X, clientY: center2Y}); + await fireEvent.mouseUp(elemDrag, {clientX: center2X, clientY: center2Y}); + +}; + +/* Drop & Move event */ +const dropAndMove = async (resizerElement: HTMLElement, moveOffest: Array<{clientX: number, clientY: number}>) => { + + await fireEvent.mouseDown(resizerElement, moveOffest[0]); + await fireEvent.mouseMove(resizerElement, moveOffest[1]); + await fireEvent.mouseUp(resizerElement, moveOffest[1]) + +} + +/* Mouse hover/drop event */ +const mouseDrop = async (element:HTMLElement) => { + await fireEvent.mouseDown(element); +} + +/* Mouse up event */ +const mouseUp = async (element:HTMLElement) => { + await fireEvent.mouseUp(element); +} +export { dragAndDrop, dropAndMove, mouseDrop, mouseUp }; \ No newline at end of file diff --git a/packages/ui-kit/__mocks__/styleMock.ts b/packages/ui-kit/__mocks__/styleMock.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/ui-kit/__mocks__/styleMock.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/ui-kit/config/jest.js b/packages/ui-kit/config/jest.js deleted file mode 100644 index 3956c6eb3..000000000 --- a/packages/ui-kit/config/jest.js +++ /dev/null @@ -1,2 +0,0 @@ -import React from "react"; -global.React = React; \ No newline at end of file diff --git a/packages/ui-kit/jest.config.ts b/packages/ui-kit/jest.config.ts new file mode 100644 index 000000000..aecc4e6d2 --- /dev/null +++ b/packages/ui-kit/jest.config.ts @@ -0,0 +1,25 @@ +import React from "react"; +import type {Config} from 'jest'; + +global.React = React; + +const esModules = ["nanoid"].join("|"); + +const config: Config = { + testEnvironment: "jsdom", + preset: 'ts-jest', + transform: { + '^.+\\.(ts|tsx)?$': ['ts-jest', { + babelConfig: true, + }], + "^.+\\.(js|jsx)$": "babel-jest" + }, + transformIgnorePatterns: [`/node_modules/(?!${esModules})`], + moduleNameMapper: { + "\\.(css|sass)$": "/__mocks__/styleMock.ts", + "^uuid$": "uuid", + "^nanoid(/(.*)|$)": "nanoid$1", + }, +}; + +export default config; \ No newline at end of file diff --git a/packages/ui-kit/package.json b/packages/ui-kit/package.json index 336d72a45..8c1c4bd36 100644 --- a/packages/ui-kit/package.json +++ b/packages/ui-kit/package.json @@ -27,7 +27,7 @@ "test:generate-output": "jest --json --outputFile=.jest-test-results.json || true", "test": "jest", "test-storybook": "test-storybook", - "start:storybook": "npm run test:generate-output && yarn storybook", + "start:storybook": "yarn test:generate-output && yarn storybook", "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook", "build:tailwind": "postcss src/scss/base.scss -o src/scss/tailwind.scss", @@ -41,6 +41,7 @@ "devDependencies": { "@babel/cli": "^7.16.0", "@babel/core": "^7.14.8", + "@babel/preset-env": "^7.19.4", "@storybook/addon-actions": "^6.5.10", "@storybook/addon-essentials": "^6.5.10", "@storybook/addon-interactions": "^6.5.10", @@ -60,6 +61,7 @@ "@types/markdown-it": "^12.2.3", "@types/react": "^17.0.14", "@types/react-table": "^7.7.10", + "babel-jest": "^29.1.2", "babel-loader": "^8.2.2", "babel-preset-react-app": "^10.0.0", "chromatic": "^6.7.0", @@ -76,6 +78,7 @@ "sass-loader": "^13.0.2", "style-loader": "^3.3.1", "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17", + "ts-jest": "^29.0.3", "typescript": "^4.3.5" }, "dependencies": { @@ -99,8 +102,6 @@ "react-codemirror2": "^7.2.1", "react-complex-tree": "^1.1.11", "react-custom-scrollbars": "^4.2.1", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", "react-full-screen": "^0.3.1-0", "react-hook-form": "^6.8.1", "react-reflex": "^4.0.3", @@ -113,24 +114,11 @@ }, "peerDependencies": { "react": "17.0.2", - "react-dnd": "^16.0.0", - "react-dnd-html5-backend": "^16.0.0", "react-dom": "17.0.2" }, "babel": { "presets": [ - [ - "react-app", - { - "absoluteRuntime": false - } - ] - ] - }, - "jest": { - "testEnvironment": "jsdom", - "setupFilesAfterEnv": [ - "/config/jest.js" + "@babel/preset-env" ] } } diff --git a/packages/ui-kit/src/components/table/bulk-edit-ift/BulkEditIFT.tsx b/packages/ui-kit/src/components/table/bulk-edit-ift/BulkEditIFT.tsx index d5452213f..a1ca07efa 100644 --- a/packages/ui-kit/src/components/table/bulk-edit-ift/BulkEditIFT.tsx +++ b/packages/ui-kit/src/components/table/bulk-edit-ift/BulkEditIFT.tsx @@ -12,9 +12,6 @@ import { IBulkEditIFT } from '../interfaces/BulkEditIFT.interfaces'; import { _table } from '@firecamp/utils'; -import Table from '../table/Table'; -import { defaultData , columnDataForDisplay, headerRow, headerColumnDataForDisplay, TableInput} from '../table/TableData'; - const modes = { TABLE: 'table', RAW: 'raw', @@ -35,12 +32,6 @@ const BulkEditIFT: FC = ({ let [mode, setMode] = useState(modes.TABLE); let [raw, setRaw] = useState(''); - - let [tableValue, setTableValue] = useState(defaultData); - useEffect(() => { - onChange([headerRow]) - },[]) - useEffect(() => { try { if (mode === modes.RAW) { @@ -79,12 +70,6 @@ const BulkEditIFT: FC = ({ } }; - let updateTableData = (newRows: any [] = []) => { - if (!equal(newRows, tableValue)) { - setTableValue(newRows); - } - } - return (
@@ -114,8 +99,8 @@ const BulkEditIFT: FC = ({ name={title} meta={meta} disabled={disabled} - custom={true} - columnDetails={headerColumnDataForDisplay} + // custom={true} + // columnDetails={headerColumnDataForDisplay} /> ) : (
@@ -134,24 +119,6 @@ const BulkEditIFT: FC = ({
)} - <>{row}} - cellRenderer={( cell ) => } - /> - - - ); }; diff --git a/packages/ui-kit/src/components/table/table/Table.stories.tsx b/packages/ui-kit/src/components/table/table/Table.stories.tsx index e75670ae2..07cfe88e6 100644 --- a/packages/ui-kit/src/components/table/table/Table.stories.tsx +++ b/packages/ui-kit/src/components/table/table/Table.stories.tsx @@ -1,12 +1,10 @@ //@ts-nocheck import { useState } from 'react'; import equal from 'deep-equal'; -import { screen, userEvent } from '@storybook/testing-library'; -import { within, fireEvent } from '@testing-library/react'; -import {expect} from "@storybook/jest"; +import { within } from '@testing-library/react'; import Table from './Table'; -import { defaultData, columnDataForDisplay, TableInput, dragAndDrop, TableColumnHeading } from "./TableData"; +import { defaultData, columnDataForDisplay, TableInput, TableColumnHeading } from "./TableData"; import { _array } from '@firecamp/utils' export default { @@ -63,56 +61,8 @@ SimpleTable.args = { SimpleTable.play = async ({canvasElement}) => { const canvas = within(canvasElement); - //table is rendered const renderedTable = await canvas.getByRole('table'); - expect(renderedTable).toBeInTheDocument(); - - //table columns should be same as provided in columns - const columnHeading = within(renderedTable).getAllByRole('columnheader') - expect(columnHeading).toHaveLength(columnDataForDisplay.length) - - //column heading value should be same as provided in displayName - const columnHeadingDifferFromDisplayName = columnHeading.filter((data, index) => columnDataForDisplay[index].displayName !== data.textContent) - expect(columnHeadingDifferFromDisplayName).toHaveLength(0); - - //validate the rows rendered into the table - const tableBody = await within(renderedTable).getAllByRole("rowgroup")[1] - const tableRows = await within(tableBody).findAllByRole("row") - expect(tableRows).toHaveLength(defaultData.length); - - //validate all rows are sortable - tableRows.map(row => expect(row.draggable).toBeTruthy()); - - //on mousedown/touch start on column header resizer div should update the styles of the element - const columnResizer = await canvas.queryAllByTestId('col-resizer'); - if(!_array.isEmpty(columnResizer)){ - - await fireEvent.mouseDown(columnResizer[0]); - expect(columnResizer[0].className).toBe("pt-resizer h-full pt-resizing"); - await fireEvent.mouseUp(columnResizer[0]); - - //table column resize logic : column width is updating along with resizer div's offsetLeft value - const resizerElement = columnResizer[2] , resizerElementColumnWidth = resizerElement.parentNode; - const intialColumnWidth = resizerElementColumnWidth.offsetWidth; - const columnMouseMoveOffset = [{ clientX: 144, clientY: 0 },{ clientX: 200, clientY: 0 }] - - await fireEvent.mouseDown(resizerElement, columnMouseMoveOffset[0]); - await fireEvent.mouseMove(resizerElement, columnMouseMoveOffset[1]); - await fireEvent.mouseUp(resizerElement, columnMouseMoveOffset[1]) - - expect(resizerElementColumnWidth.offsetWidth).toBeGreaterThan(intialColumnWidth); - } - - //table row sorting logic : first row location should be shifted to last using the drag button's row key - const dragIndex = 0, dropIndex = 2; - const rowSorter = await within(tableBody).findAllByTestId('row-sorter'); - const initialRowId = rowSorter[dragIndex].parentNode.id; - dragAndDrop(rowSorter[dragIndex], rowSorter[dropIndex]); - const rowSorted = await within(tableBody).findAllByTestId('row-sorter'); - const updatedRowId = rowSorted[dropIndex].parentNode.id; - - expect(initialRowId).toBe(updatedRowId) }; diff --git a/packages/ui-kit/src/components/table/table/Table.test.tsx b/packages/ui-kit/src/components/table/table/Table.test.tsx new file mode 100644 index 000000000..922933dcf --- /dev/null +++ b/packages/ui-kit/src/components/table/table/Table.test.tsx @@ -0,0 +1,98 @@ +import {render, screen, waitFor} from "@testing-library/react"; +import "@testing-library/jest-dom"; +import {SimpleTable} from "./Table.stories"; +import ResizeObserver from "../../../../__mocks__/ResizeObserver"; +import {dragAndDrop, dropAndMove, mouseDrop, mouseUp} from "../../../../__mocks__/eventMock"; +import { _array } from '@firecamp/utils'; + +window.ResizeObserver = ResizeObserver; + +describe("Table : " , () => { + + const COLUMNS_PROVIDED = SimpleTable.args.columns; + const ROWS_PROVIDED = SimpleTable.args.data; + + const mountTableComponent = () => render(); + const getRenderedTable = () => screen.getByRole('table'); + + const getAllColumnHeading = () => screen.getAllByRole('columnheader'); + const getAllColumnHeadingResizableElement = () => screen.findAllByTestId('col-resizer'); + + const getRenderedTableRow = () => screen.findAllByRole("row"); + const getAllSortableRow = async () => (await screen.findAllByTestId('row-sorter')).map(ele => ele.parentElement); + const getAllSortableRowElement = () => screen.findAllByTestId('row-sorter'); + + test('Table should render', () => { + mountTableComponent(); + expect(getRenderedTable()).toBeInTheDocument(); + }); + + test('Table columns should be same as provided in COLUMNS_PROVIDED(columns)', () => { + mountTableComponent(); + expect(getAllColumnHeading()).toHaveLength(COLUMNS_PROVIDED.length) + }); + + test('Table columns heading value should be same as provided in displayName of columns', () => { + mountTableComponent(); + const columnHeading = getAllColumnHeading(); + const columnHeadingDifferFromDisplayName = columnHeading.filter((data, index) => COLUMNS_PROVIDED[index].displayName !== data.textContent) + expect(columnHeadingDifferFromDisplayName).toHaveLength(0); + }); + + test('Table rows should be same as provided in ROWS_PROVIDED(data)', async () => { + mountTableComponent(); + const tableRows = await getAllSortableRow(); + expect(tableRows).toHaveLength(ROWS_PROVIDED.length); + }); + + test('Table rows are sortable', async() => { + mountTableComponent(); + const tableRows = await getAllSortableRow(); + tableRows.map((row: HTMLElement) => expect(row.draggable).toBeTruthy()); + }); + + test('on mousedown/touch start on column header resizer div should update the styles of the element',async () => { + + mountTableComponent(); + const columnResizer = await getAllColumnHeadingResizableElement(); + + //table columns should be resizable + if(!_array.isEmpty(columnResizer)){ + const moveElementWidthIndex = 2; + + //table column resizer element : updating the classname on hover over the element + mouseDrop(columnResizer[moveElementWidthIndex]); + await waitFor(() => getAllColumnHeadingResizableElement()); + expect(columnResizer[moveElementWidthIndex].className).toBe("pt-resizer h-full pt-resizing"); + mouseUp(columnResizer[moveElementWidthIndex]); + + //table column resize logic : column width is updating along with resizer div's offsetLeft value + const resizerElement = columnResizer[moveElementWidthIndex]; + const intialColumnWidth = parseInt(resizerElement.parentElement.style.minWidth); + const columnMouseMoveOffset = [{ clientX: 144, clientY: 0 },{ clientX: 200, clientY: 0 }] + + dropAndMove(resizerElement, columnMouseMoveOffset); + await waitFor(() => getAllColumnHeadingResizableElement()); + let updatedColumnWidth = parseInt(resizerElement.parentElement.style.minWidth); + + expect(updatedColumnWidth).toBeGreaterThan(intialColumnWidth); + }else{ + expect(SimpleTable.args.tableResizable).toBeFalsy(); + } + }); + + test('table row sorting logic : first row location should be shifted to last using the drag buttons row key',async() => { + mountTableComponent(); + const dragIndex = 0, dropIndex = 2; + const rowSorter = await getAllSortableRowElement(); + const initialRowId = rowSorter[dragIndex].parentElement.id; + + dragAndDrop(rowSorter[dragIndex], rowSorter[dropIndex]); + await waitFor(() => getAllSortableRow()); + const rowSorted = await getAllSortableRowElement(); + const updatedRowId = rowSorted[dropIndex].parentElement.id; + + expect(initialRowId).toBe(updatedRowId); + }); + +}) diff --git a/packages/ui-kit/src/components/table/table/Table.tsx b/packages/ui-kit/src/components/table/table/Table.tsx index c69b5c52a..703d08cbb 100644 --- a/packages/ui-kit/src/components/table/table/Table.tsx +++ b/packages/ui-kit/src/components/table/table/Table.tsx @@ -1,228 +1,256 @@ -import { ColumnResizeMode, getCoreRowModel, useReactTable, flexRender, createColumnHelper } from '@tanstack/react-table'; -import React, { FC, useEffect, useState, ReactNode, useRef } from 'react'; -import TableDraggableRow from "./TableDraggableRow"; -import cx from "classnames"; -import "../../table-v3/primary-table/table.sass"; -import classNames from 'classnames'; +import { FC, useEffect, useState, ReactNode, useRef } from 'react'; +import { + ColumnResizeMode, + getCoreRowModel, + useReactTable, + flexRender, + createColumnHelper, +} from '@tanstack/react-table'; +import cx from 'classnames'; +import TableDraggableRow from './TableDraggableRow'; +import '../../table-v3/primary-table/table.sass'; const Table: FC = ({ - name = "", - data = [], - columns, - tableResizable = false, - columnRenderer = () => { }, - cellRenderer = () => { }, - tableWidth = 200, - options = {} + name = '', + data = [], + columns, + resizable = false, + columnRenderer = () => {}, + cellRenderer = () => {}, + width = 200, + options = {}, }) => { + const [tableData, setTableData] = useState(() => []); + const [dragId, setDragId] = useState(0); + const [containerWidth, setContainerWidth] = useState(width); - const [tableData, setTableData] = useState(() => []) - const [dragId, setDragId] = useState(0); - const [containerWidth, setContainerWidth] = useState(tableWidth); - - - const tableRef = useRef(null); - const containerDivRef = useRef(null); - - - const [columnResizeMode, setColumnResizeMode] = React.useState('onChange') - - const columnHelper = createColumnHelper() - - const columnDisplay = [ - ...columns.map(column => columnHelper.accessor(column.name, - { - id: column.name, - ...(typeof column.width !== "undefined" ? { size: column.width } : {}), - minSize: ( - typeof column.minSize !== "undefined" ? column.minSize : - (typeof options.minColumnSize !== "undefined" ? options.minColumnSize : 50 ) - ), - enableResizing: (typeof column.enableResizing !== "undefined") ? column.enableResizing : false, - header: (col) => { - return columnRenderer((typeof column.displayName !== "undefined" ? column.displayName : column.name)) - }, - cell: ({cell}) => { - let cellValue = cell.getValue(); - if(cell.getValue() !== "undefined"){ - return cellRenderer({cellValue, - rowIndex : cell.row.index, - columnId: cell.column.id, - column: cell.column - }) - }else{ - return<> - } - - }, - } - )) - ]; - - useEffect(() => { - setTableData(data) - }, [data]); - - //get the width of container div in pixels - useEffect(() => { - if (!containerDivRef.current) return; - const resizeObserver = new ResizeObserver(() => { - setContainerWidth(containerDivRef.current.clientWidth) - }); - resizeObserver.observe(containerDivRef.current); - return () => resizeObserver.disconnect(); - }, [containerDivRef.current]); - - const table = useReactTable({ - data: tableData, - columns: columnDisplay, - enableColumnResizing: tableResizable, - ...(tableResizable ? { columnResizeMode: columnResizeMode } : {}), - getCoreRowModel: getCoreRowModel(), + const tableRef = useRef(null); + const containerDivRef = useRef(null); + + const [columnResizeMode, setColumnResizeMode] = + useState('onChange'); + + const columnHelper = createColumnHelper(); + + const columnDisplay = [ + ...columns.map((column) => + columnHelper.accessor(column.name, { + id: column.name, + ...(typeof column.width !== 'undefined' ? { size: column.width } : {}), + minSize: + typeof column.minSize !== 'undefined' + ? column.minSize + : typeof options.minColumnSize !== 'undefined' + ? options.minColumnSize + : 50, + enableResizing: + typeof column.enableResizing !== 'undefined' + ? column.enableResizing + : false, + header: (col) => { + return columnRenderer( + typeof column.displayName !== 'undefined' + ? column.displayName + : column.name + ); + }, + cell: ({ cell }) => { + let cellValue = cell.getValue(); + if (cell.getValue() !== 'undefined') { + return cellRenderer({ + cellValue, + rowIndex: cell.row.index, + columnId: cell.column.id, + column: cell.column, + }); + } else { + return <>; + } + }, + }) + ), + ]; + + useEffect(() => { + setTableData(data); + }, [data]); + + //get the width of container div in pixels + useEffect(() => { + if (!containerDivRef.current) return; + const resizeObserver = new ResizeObserver(() => { + setContainerWidth(containerDivRef.current.clientWidth); }); + resizeObserver.observe(containerDivRef.current); + return () => resizeObserver.disconnect(); + }, [containerDivRef.current]); + const table = useReactTable({ + data: tableData, + columns: columnDisplay, + enableColumnResizing: resizable, + ...(resizable ? { columnResizeMode: columnResizeMode } : {}), + getCoreRowModel: getCoreRowModel(), + }); - function drag(rowIndex: number) { - setDragId(rowIndex); - } + function drag(rowIndex: number) { + setDragId(rowIndex); + } - //reorder the index value for the table rows - function drop(rowIndex: number) { - tableData.splice(rowIndex, 0, tableData.splice(dragId, 1)[0]) - setTableData([...tableData]) - } + //reorder the index value for the table rows + function drop(rowIndex: number) { + tableData.splice(rowIndex, 0, tableData.splice(dragId, 1)[0]); + setTableData([...tableData]); + } - return ( -
-
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + - - {table.getHeaderGroups().map(headerGroup => - - { - headerGroup.headers.map(header => { - return - }) - } - )} - - - {table.getRowModel().rows.map(row => { - return - })} - - -
-
table.getTotalSize()) ? header.getSize() + (containerWidth - table.getTotalSize() - 4) : header.getSize() - }} - > - { - header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - ) - } - { - header.column.getCanResize() && ( -
) - } -
-
) + minWidth: + header.index === columnDisplay.length - 1 && + containerWidth > table.getTotalSize() + ? header.getSize() + + (containerWidth - table.getTotalSize() - 4) + : header.getSize(), + }} + > + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + {header.column.getCanResize() && ( +
+ )} + + ); + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + ); + })} + + +
+ ); }; export default Table; -export type { ITable, ITableRow, TPlainObject } - -const Th: FC = ({ children = <>, className = "", style = {} }) => { - - return ( - - {children} - - ) -} - -export const Td: FC = ({ children, className = "", style = {} }) => { - return ( - - {children} - - ) -} - -type TPlainObject = { [K: string]: any } +export type { ITable, ITableRow, TPlainObject }; + +const Th: FC = ({ children = <>, className = '', style = {} }) => { + return ( + + {children} + + ); +}; + +export const Td: FC = ({ children, className = '', style = {} }) => { + return ( + + {children} + + ); +}; + +type TPlainObject = { [K: string]: any }; type ITable = { - /** - * Provide a unique id for the table - */ - name: string, - /** - * Array of data to be rendered in table - */ - data: Array, - /** - * Allow table columns to resize - */ - tableResizable: boolean, - /** - * Array of columns to be rendered, column should follow IColumn type - */ - columns: Array, - /** - * Component to format the column headings - */ - columnRenderer: (column: object | string) => string | JSX.Element, - /** - * Component to format the table cell - */ - cellRenderer: (cell: object) => string | JSX.Element, - /** - * Minimum width for the table component - */ - tableWidth: number, - /** - * Optional functionality for table
- * containerClassName : classname for table wrapper div - * minColumnSize : minimum column width (if not defined in columns object) - */ - options: ITableOptions -} + /** provide a unique id for the table */ + name: string; + + /** array of columns to be rendered, column should follow IColumn type */ + columns: Array; + + /** array of data to be rendered in table */ + data: Array; + + /** allow table columns to resize*/ + resizable: boolean; + + /** Component to format the column headings*/ + columnRenderer: (column: object | string) => string | JSX.Element; + + /** Component to format the table cell */ + cellRenderer: (cell: object) => string | JSX.Element; + + /** Minimum width for the table component*/ + width: number; + + /** table options */ + options: ITableOptions; +}; type ITableOptions = { - containerClassName?: string, - minColumnSize?: number -} + /** class name for table wrapper div */ + containerClassName?: string; + + /** minimum column width (if not defined in columns object) */ + minColumnSize?: number; +}; type IColumn = { - name: string, - displayName?: string, - width?: number, - minSize?:number, - maxSize?:number, - enableResizing?: boolean -} + name: string; + displayName?: string; + width?: number; + minSize?: number; + maxSize?: number; + enableResizing?: boolean; +}; type ITableRow = { - row: TPlainObject, - index?: number, - handleDrop: Function, - handleDrag: Function -} -type ITh = { children: ReactNode, className?: string, style?: TPlainObject } -type ITd = { children: ReactNode, className?: string, style?: TPlainObject } \ No newline at end of file + row: TPlainObject; + index?: number; + handleDrop: Function; + handleDrag: Function; +}; +type ITh = { children: ReactNode; className?: string; style?: TPlainObject }; +type ITd = { children: ReactNode; className?: string; style?: TPlainObject }; diff --git a/packages/ui-kit/src/components/table/table/TableData.tsx b/packages/ui-kit/src/components/table/table/TableData.tsx index 4cd8e557d..aca20b63b 100644 --- a/packages/ui-kit/src/components/table/table/TableData.tsx +++ b/packages/ui-kit/src/components/table/table/TableData.tsx @@ -1,205 +1,143 @@ -import { Td, TPlainObject } from "./Table"; +import { Td, TPlainObject } from './Table'; import { useEffect, useState } from 'react'; export type Person = { - key: string, - value: string, - description: string, - popularPlace: string, - pincode: number -} + key: string; + value: string; + description: string; + popularPlace: string; + pincode: number; +}; export const defaultData: Person[] = [ { - key: "City 1", - value: "Ahmedabad", - description: "Ahmedabad, in western India, is the largest city in the state of Gujarat. ", - popularPlace: "Kankaria Lake", - pincode: 380001 + key: 'City 1', + value: 'Ahmedabad', + description: + 'Ahmedabad, in western India, is the largest city in the state of Gujarat. ', + popularPlace: 'Kankaria Lake', + pincode: 380001, }, { - key: "City 2", - value: "Surat", - description: "Surat is a large city beside the Tapi River in the west Indian state of Gujarat", - popularPlace: "Dumas Beach", - pincode: 395003 + key: 'City 2', + value: 'Surat', + description: + 'Surat is a large city beside the Tapi River in the west Indian state of Gujarat', + popularPlace: 'Dumas Beach', + pincode: 395003, }, { - key: "City 3", - value: "Mahemdavad", - description: "Mahemdavad is a town with municipality in the Kheda district in the Indian state of Gujarat", - popularPlace: "Siddhivinayak Temple", - pincode: 387130 + key: 'City 3', + value: 'Mahemdavad', + description: + 'Mahemdavad is a town with municipality in the Kheda district in the Indian state of Gujarat', + popularPlace: 'Siddhivinayak Temple', + pincode: 387130, }, ]; export function getData() { - return defaultData -}; + return defaultData; +} //For keeping column as static - provide minSize & width without resizing param export const columnDataForDisplay = [ { - name: "action", - displayName: " ", + name: 'action', + displayName: ' ', minSize: 64, - width: 64 + width: 64, }, { - name: "value", - displayName: "City", + name: 'value', + displayName: 'City', minSize: 145, enableResizing: true, }, { - name: "description", - displayName: "Description", + name: 'description', + displayName: 'Description', minSize: 145, enableResizing: true, }, { - name: "popularPlace", - displayName: "Location", + name: 'popularPlace', + displayName: 'Location', enableResizing: true, }, { - name: "pincode", + name: 'pincode', minSize: 60, - displayName: "Area Code", + displayName: 'Area Code', }, -] - +]; export const headerRow = { - description: "Description", + description: 'Description', disable: false, - key: "test", - type: "text", - value: "Value here" -} + key: 'test', + type: 'text', + value: 'Value here', +}; export const headerColumnDataForDisplay = [ { - name: "action", - displayName: " ", + name: 'action', + displayName: ' ', minSize: 64, - width: 64 + width: 64, }, { - name: "key", - displayName: "Key", + name: 'key', + displayName: 'Key', minSize: 145, enableResizing: true, }, { - name: "value", - displayName: "Value", + name: 'value', + displayName: 'Value', minSize: 145, enableResizing: true, }, { - name: "description", - displayName: "Description", - minSize: 145 - } -] + name: 'description', + displayName: 'Description', + minSize: 145, + }, +]; -export const TableColumnHeading = ({heading}: TPlainObject) => { - return <>{heading} -} +export const TableColumnHeading = ({ heading }: TPlainObject) => { + return <>{heading}; +}; export const TableInput = (props: any) => { - let { onChange, autoFocus, cell, rows } = props + let { onChange, autoFocus, cell, rows } = props; const [inputValue, setInputValue] = useState(cell.cellValue); - return - + className={ + ' h-[30px] relative overflow-hidden overflow-ellipsis whitespace-nowrap align-baseline' + } + > { - setInputValue(e.target.value);}} - onBlur={(e) => { - let updatedRow = Object.assign([],rows); - updatedRow[cell.rowIndex] = {...updatedRow[cell.rowIndex], [cell.columnId]: e.target.value} - onChange(updatedRow) + setInputValue(e.target.value); + }} + onBlur={(e) => { + let updatedRow = Object.assign([], rows); + updatedRow[cell.rowIndex] = { + ...updatedRow[cell.rowIndex], + [cell.columnId]: e.target.value, + }; + onChange(updatedRow); }} className="text-appForeground bg-appBackground h-[29px] w-full absolute top-0 left-0 !border-0 p-1 text-base overflow-ellipsis focus:!border-0" /> -} - -/* Drag & drop event for testing */ - -const fireMouseEvent = function ( - type: string, - elem: EventTarget, - centerX: number, - centerY: number -) { - const evt = new MouseEvent(type, { - bubbles:true, - cancelable: true, - view: window, - detail: 1, - screenX: 1, - screenY: 1, - clientX: centerX, - clientY: centerY, - ctrlKey: false, - altKey: false, - shiftKey: false, - metaKey: false, - button: 0, - relatedTarget: elem - }); - return elem.dispatchEvent(evt); + ); }; - -export const dragAndDrop = (elemDrag: HTMLElement, elemDrop: HTMLElement) => { - // calculate positions - let pos = elemDrag.getBoundingClientRect(); - const center1X = Math.floor((pos.left + pos.right) / 2); - const center1Y = Math.floor((pos.top + pos.bottom) / 2); - - pos = elemDrop.getBoundingClientRect(); - const center2X = Math.floor((pos.left + pos.right) / 2); - const center2Y = Math.floor((pos.top + pos.bottom) / 2); - - // mouse over dragged element and mousedown - fireMouseEvent('mousemove', elemDrag, center1X, center1Y); - fireMouseEvent('mouseenter', elemDrag, center1X, center1Y); - fireMouseEvent('mouseover', elemDrag, center1X, center1Y); - fireMouseEvent('mousedown', elemDrag, center1X, center1Y); - - // start dragging process over to drop target - const dragStarted = fireMouseEvent( - 'dragstart', - elemDrag, - center1X, - center1Y - ); - if (!dragStarted) { - return; - } - - fireMouseEvent('drag', elemDrag, center1X, center1Y); - fireMouseEvent('mousemove', elemDrag, center1X, center1Y); - fireMouseEvent('drag', elemDrag, center2X, center2Y); - fireMouseEvent('mousemove', elemDrop, center2X, center2Y); - - // trigger dragging process on top of drop target - fireMouseEvent('mouseenter', elemDrop, center2X, center2Y); - fireMouseEvent('dragenter', elemDrop, center2X, center2Y); - fireMouseEvent('mouseover', elemDrop, center2X, center2Y); - fireMouseEvent('dragover', elemDrop, center2X, center2Y); - - // release dragged element on top of drop target - fireMouseEvent('drop', elemDrop, center2X, center2Y); - fireMouseEvent('dragend', elemDrag, center2X, center2Y); - fireMouseEvent('mouseup', elemDrag, center2X, center2Y); - -}; \ No newline at end of file diff --git a/packages/ui-kit/src/components/table/table/TableDraggableRow.tsx b/packages/ui-kit/src/components/table/table/TableDraggableRow.tsx index 9e42599ab..c3ac90439 100644 --- a/packages/ui-kit/src/components/table/table/TableDraggableRow.tsx +++ b/packages/ui-kit/src/components/table/table/TableDraggableRow.tsx @@ -1,34 +1,38 @@ -import { flexRender, Row } from "@tanstack/react-table"; -import { FC, Fragment } from "react"; -import { ITableRow, TPlainObject } from "./Table"; +import { flexRender, Row } from '@tanstack/react-table'; +import { FC, Fragment } from 'react'; +import { GrDrag } from '@react-icons/all-files/gr/GrDrag'; +import { ITableRow, TPlainObject } from './Table'; -const TableDraggableRow : FC = (props) => { +const TableDraggableRow: FC = (props) => { + let { row, handleDrag, handleDrop } = props; - let { row, handleDrag, handleDrop } = props - - return ( handleDrag(row.index)} - draggable={true}> - - { - row.getVisibleCells().map((cell: TPlainObject) => { - return - {(cell.column.columnDef.accessorKey === "action") ? - (e.preventDefault(),handleDrop(row.index))} - onDragOver={(e) => e.preventDefault()}> - - - : flexRender(cell.column.columnDef.cell, cell.getContext())} - - }) - } + return ( + handleDrag(row.index)} + draggable={true} + > + {row.getVisibleCells().map((cell: TPlainObject) => { + return ( + + {cell.column.columnDef.accessorKey === 'action' ? ( + (e.preventDefault(), handleDrop(row.index))} + onDragOver={(e) => e.preventDefault()} + > + + + ) : ( + flexRender(cell.column.columnDef.cell, cell.getContext()) + )} + + ); + })} - ) -} - + ); +}; -export default TableDraggableRow; \ No newline at end of file +export default TableDraggableRow; diff --git a/packages/ui-kit/src/components/tabs/Tabs.tsx b/packages/ui-kit/src/components/tabs/Tabs.tsx index 11e6d2b05..e58eaa85a 100644 --- a/packages/ui-kit/src/components/tabs/Tabs.tsx +++ b/packages/ui-kit/src/components/tabs/Tabs.tsx @@ -102,11 +102,10 @@ const Tabs: FC = ({ {sortedList.map((tab, i) => { return ( ; + + /** callback on drop */ + onTabDrop?: DragEventHandler; + + height?: number; + tabVersion?: number; + tabIndex?: number; +} + +/** Tab close icon configuration to set icon position, visibility, click event, and disabled property */ +export interface ICloseTabIconMeta { + /** set true if need top show close icon */ + show?: boolean; + + /** a function to call upon close icon click */ + onClick?: (tabId: TId, index: number) => void; + + /** prevent click event for close icon if true*/ + disabled?: boolean; +} + +/** Tab border configuration to set border position, and visibility */ +export interface ITabBorderMeta { + /** active tab border placement */ + placementForActive?: EActiveBorderPosition; + right?: boolean; +} + +/** active tab border placement */ +export enum EActiveBorderPosition { + Top = 'top', + Bottom = 'bottom', +} diff --git a/packages/ui-kit/src/components/tabs/v3/Tab.tsx b/packages/ui-kit/src/components/tabs/v3/Tab.tsx new file mode 100644 index 000000000..777b64f52 --- /dev/null +++ b/packages/ui-kit/src/components/tabs/v3/Tab.tsx @@ -0,0 +1,198 @@ +import { FC } from 'react'; +import cx from 'classnames'; +import { VscClose } from '@react-icons/all-files/vsc/VscClose'; +import { VscCircleFilled } from '@react-icons/all-files/vsc/VscCircleFilled'; + +import { EActiveBorderPosition, ITab } from './Tab.interface'; +import Count from '../Count'; +import '../Tabs.scss'; + +const TopBorderPlacement = ({ show = false }) => + show ? ( +
+ ) : ( + <> + ); + +const BottomBorderPlacement = ({ show = false }) => + show && ( +
+ ); + +const TitlePlacement = ({ + name, + isPreview, +}: { + name: string; + isPreview: boolean; +}) =>
{name}
; + +const CloseIconPlacement = ({ + id = '', + state = 'default', + onClick = (e: any) => {}, + show = false, +}: { + id: string; + state: string; + onClick: Function; + show: boolean; +}) => + show && ( +
+ {state == 'modified' ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ ); + +const Tab: FC = ({ + id, + index, + className = '', + name = '', + state = 'default', + isPreview = false, + draggable = false, + borderMeta = { + placementForActive: 'top', + right: true, + }, + closeTabIconMeta = { + show: true, + onClick: () => {}, + disabled: false, + }, + isActive = false, + onSelect = () => {}, + preComp, + postComp, + count, + height, + dotIndicator, + tabVersion, + + onTabDragStart, + onTabDrop, + ...tabProps +}) => { + return ( + + ); +}; + +export default Tab; + +Tab.defaultProps = { + id: '', + name: '', + state: 'default', + isPreview: false, + draggable: false, + borderMeta: { + placementForActive: EActiveBorderPosition.Top, + right: true, + }, + closeTabIconMeta: { + show: true, + onClick: () => {}, + disabled: false, + }, + isActive: false, + onSelect: () => {}, +}; diff --git a/packages/ui-kit/src/components/tabs/v3/Tabs.interface.ts b/packages/ui-kit/src/components/tabs/v3/Tabs.interface.ts new file mode 100644 index 000000000..8adc02520 --- /dev/null +++ b/packages/ui-kit/src/components/tabs/v3/Tabs.interface.ts @@ -0,0 +1,75 @@ +import { ReactNode } from 'react'; +import { TId } from '@firecamp/types'; +import { ITab, ICloseTabIconMeta, ITabBorderMeta } from './Tab.interface'; + +export interface ITabs { + /** an unique identifier */ + id?: string; + + /** add class name to show custom styling */ + className?: string; + + /** tabs */ + list: Record; + + /** tab orders */ + orders: TId[]; + + /** tabs pre component, render before tab title */ + preComp?: ReactNode; + + /** tabs post component, render at last */ + postComp?: ReactNode; + + /** tabs post component, render just after the name/title */ + suffixComp?: ReactNode; + + /** active tab id */ + activeTab?: string; + + /** close icon meta for tab */ + closeTabIconMeta?: ICloseTabIconMeta; + + /** add icon meta to add new tab*/ + addTabIconMeta?: IAddTabIconMeta; + + /** tab border meta to configure tab border **/ + tabBorderMeta?: ITabBorderMeta; + + /** a callback function to call when tab is being clicked/selected*/ + onSelect?: (id: string | number, index: number, event: any) => void; + + /** allow reorder */ + reOrderable: boolean; + + /** callback on tabs' reorder */ + onReorder?: (tabIds: TId[]) => void; + + withDivider?: boolean; + + /** height of the tab panel */ + height?: number; + + /** tab index for accessibility */ + tabIndex: number; + + /** Manage className/UI by tabVersion, [1, 2] */ + tabsVersion?: number; + + /** apply equal widths to all tabs */ + equalWidth?: boolean; +} + +/** Tab add icon configuration to set icon position, visibility, click event, and disabled property */ +export interface IAddTabIconMeta { + id?: string; + + /** show add icon if true */ + show?: boolean; + + /** callback function for add icon click */ + onClick?: Function; + + /** prevent click event if true */ + disabled?: boolean; +} diff --git a/packages/ui-kit/src/components/tabs/v3/Tabs.tsx b/packages/ui-kit/src/components/tabs/v3/Tabs.tsx new file mode 100644 index 000000000..f7806095b --- /dev/null +++ b/packages/ui-kit/src/components/tabs/v3/Tabs.tsx @@ -0,0 +1,304 @@ +import { + DragEventHandler, + FC, + forwardRef, + Fragment, + useImperativeHandle, + useRef, + useState, +} from 'react'; +import cx from 'classnames'; +import { VscAdd } from '@react-icons/all-files/vsc/VscAdd'; +import { TId } from '@firecamp/types'; + +import Tab from './Tab'; +import { ITabs } from './Tabs.interface'; +import { EActiveBorderPosition, ITab } from './Tab.interface'; + +const Tabs: FC = forwardRef( + ( + { + id, + className, + list, + orders: _orders, + preComp, + postComp, + suffixComp, + withDivider, + activeTab: _activeTab, + equalWidth, + height, + tabsVersion, + tabIndex, + closeTabIconMeta, + addTabIconMeta, + tabBorderMeta, + reOrderable, + + onSelect = () => {}, + onReorder, + }, + ref + ) => { + const dndIds = useRef({ dragId: null, dropId: null }); + const [state, setState] = useState({ + tabs: list, + activeTab: _activeTab, + orders: _orders, + }); + + useImperativeHandle( + ref, + () => { + return { + changeName: (tabId: TId, name: string) => { + setState((s) => ({ + ...s, + tabs: { + ...s.tabs, + [tabId]: { ...s.tabs[tabId], name }, + }, + })); + }, + reorder: () => {}, + add: (tab: ITab) => { + setState((s) => ({ + ...s, + tabs: { + ...s.tabs, + [tab.id]: tab, + }, + activeTab: tab.id, + orders: [...s.orders, tab.id], + })); + }, + close: (tabId_s: TId | TId[]) => { + setState((s) => { + let orders = s.orders; + if (typeof tabId_s == 'string') { + delete s.tabs[tabId_s]; + orders = orders.filter((i) => i != tabId_s); + } else { + tabId_s.forEach((id) => { + delete s.tabs[id]; + }); + orders = orders.filter((i) => !tabId_s.includes(i)); + } + const activeTab = orders.includes(s.activeTab) + ? s.activeTab + : orders[orders.length - 1]; + return { tabs: s.tabs, orders, activeTab }; + }); + }, + }; + }, + [] + ); + + const _onSelect = (tabId: TId, index: number, e: any) => { + setState((s) => ({ + ...s, + activeTab: tabId, + })); + onSelect(tabId, index, e); + }; + + /** set dragId on drag start at Tabs */ + const onDragStart: DragEventHandler = (e) => { + dndIds.current.dragId = e.currentTarget.id; + }; + + /** set dropId on drop at Tabs and reorder ids/tabs */ + const onDrop: DragEventHandler = (e) => { + dndIds.current.dropId = e.currentTarget.id; + reorder(); + }; + + const reorder = async () => { + const { dragId, dropId } = dndIds.current; + const dragIndex = state.orders.findIndex((i) => i == dragId); + const dropIndex = state.orders.findIndex((i) => i == dropId); + + const orders = [...state.orders]; + orders.splice(dropIndex, 0, orders.splice(dragIndex, 1)[0]); + + setState((s) => ({ + ...s, + orders, + })); + dndIds.current = { dragId: null, dropId: null }; + if (typeof onReorder == 'function') onReorder(orders); + }; + + return ( +
+ +
+
+
+ {state.orders.map((tabId, i) => { + const tab = state.tabs[tabId]; + // console.log(state.tabs, tabId, 555); + if (!tab) return ; + return ( + + ); + })} + + {addTabIconMeta?.show && ( +
{ + if (!addTabIconMeta?.disabled) { + addTabIconMeta?.onClick?.(e); + } + }} + > + +
+ )} +
+
+
+ + +
+ ); + } +); + +const PreComponent: FC> = ({ preComp, tabsVersion }) => { + if (!preComp) return <>; + return ( +
+ {preComp} +
+ ); +}; + +const SuffixComponent: FC> = ({ suffixComp, tabsVersion }) => { + if (!suffixComp) return <>; + return ( +
+ {suffixComp} +
+ ); +}; + +const PostComponent: FC> = ({ postComp, tabsVersion }) => { + if (!postComp) return <>; + return ( +
+ {postComp} +
+ ); +}; + +Tabs.defaultProps = { + list: {}, + orders: [], + withDivider: false, + equalWidth: false, + reOrderable: false, + height: 32, + tabsVersion: 1, + tabIndex: 1, + + closeTabIconMeta: { + show: false, + onClick: () => {}, + disabled: false, + }, + addTabIconMeta: { + id: '', + show: false, + onClick: () => {}, + disabled: false, + }, + tabBorderMeta: { + placementForActive: EActiveBorderPosition.Top, + right: true, + }, +}; + +export default Tabs; diff --git a/packages/ui-kit/src/components/url/components/UrlBar.tsx b/packages/ui-kit/src/components/url/components/UrlBar.tsx index 2241d3f32..ab6ceea86 100644 --- a/packages/ui-kit/src/components/url/components/UrlBar.tsx +++ b/packages/ui-kit/src/components/url/components/UrlBar.tsx @@ -13,7 +13,7 @@ const UrlBar: FC & { <>
{!!nodePath ? ( -
+
{nodePath || ''} { showEditIcon? : <> } diff --git a/packages/ui-kit/src/scss/base.scss b/packages/ui-kit/src/scss/base.scss index 422062549..bac983f67 100644 --- a/packages/ui-kit/src/scss/base.scss +++ b/packages/ui-kit/src/scss/base.scss @@ -5,7 +5,8 @@ @import url(../components/split-view/SplitView.scss); @import url(collection.scss); -@import url('https://fonts.googleapis.com/css2?family=Lato:wght@100&display=swap'); +@import url('https://fonts.googleapis.com/css?family=Lato:300,400,700,900&display=swap'); +/* @import url('https://fonts.googleapis.com/css2?family=Lato:wght@100&display=swap'); */ @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;0,800;1,300;1,400;1,600;1,700;1,800&display=swap'); @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap'); @@ -730,10 +731,11 @@ body .ace_editor .ace_scrollbar{ .fc-urlbar-path,.fc-tab-panel-info > span,.smart-table-row-cell,.fc-history-req-list-item-url-text,.collection_leaf-node-name{ user-select: text; } - - // .json-as-form input.json_row--key, .json-as-form input.json_row--value{ - // background: transparent; - // } + /* + * // .json-as-form input.json_row--key, .json-as-form input.json_row--value{ + * // background: transparent; + * // } + */ /*----------other css end--------*/ /*---code mirror css starts -------*/ diff --git a/packages/ui-kit/src/scss/tailwind.scss b/packages/ui-kit/src/scss/tailwind.scss index 1687a81fd..08108f042 100644 --- a/packages/ui-kit/src/scss/tailwind.scss +++ b/packages/ui-kit/src/scss/tailwind.scss @@ -1155,15 +1155,15 @@ video { .h-3 { height: 0.75rem; } +.h-28 { + height: 7rem; +} .h-\[30px\] { height: 30px; } .h-\[29px\] { height: 29px; } -.h-28 { - height: 7rem; -} .h-7 { height: 1.75rem; } @@ -1207,6 +1207,7 @@ video { width: 100%; } .w-max { + width: -webkit-max-content; width: -moz-max-content; width: max-content; } @@ -1222,6 +1223,9 @@ video { .\!w-full { width: 100% !important; } +.w-16 { + width: 4rem; +} .w-screen { width: 100vw; } @@ -1322,18 +1326,15 @@ video { .overflow-auto { overflow: auto; } -.\!overflow-hidden { - overflow: hidden !important; -} .overflow-hidden { overflow: hidden; } +.\!overflow-hidden { + overflow: hidden !important; +} .overflow-visible { overflow: visible; } -.overflow-x-auto { - overflow-x: auto; -} .overflow-ellipsis { text-overflow: ellipsis; } @@ -1352,6 +1353,9 @@ video { .rounded-full { border-radius: 9999px; } +.rounded { + border-radius: 0.25rem; +} .rounded-3xl { border-radius: 1.5rem; } @@ -2272,7 +2276,8 @@ video { @import url(../components/split-view/SplitView.scss); @import url(collection.scss); -@import url('https://fonts.googleapis.com/css2?family=Lato:wght@100&display=swap'); +@import url('https://fonts.googleapis.com/css?family=Lato:300,400,700,900&display=swap'); +/* @import url('https://fonts.googleapis.com/css2?family=Lato:wght@100&display=swap'); */ @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;0,800;1,300;1,400;1,600;1,700;1,800&display=swap'); @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap'); @@ -2308,10 +2313,12 @@ hr{ height: 100%; } .h-fit{ + height: -webkit-fit-content; height: -moz-fit-content; height: fit-content; } .\!h-fit{ + height: -webkit-fit-content !important; height: -moz-fit-content !important; height: fit-content !important; } @@ -2354,6 +2361,7 @@ textarea:focus,textarea:focus-visible{ color: var(--error); } .w-fit { + width: -webkit-fit-content; width: -moz-fit-content; width: fit-content; } @@ -2636,6 +2644,7 @@ textarea:focus,textarea:focus-visible{ } .collapsed { + height: -webkit-fit-content !important; height: -moz-fit-content !important; height: fit-content !important; min-height: 0px !important @@ -2803,10 +2812,11 @@ body .ace_editor .ace_scrollbar{ -moz-user-select: text; user-select: text; } - - .json-as-form input.json_row--key, .json-as-form input.json_row--value{ - background: transparent; - } + /* + * // .json-as-form input.json_row--key, .json-as-form input.json_row--value{ + * // background: transparent; + * // } + */ /*----------other css end--------*/ /*---code mirror css starts -------*/ @@ -2863,27 +2873,6 @@ body .ace_editor .ace_scrollbar{ /*---code mirror css ends----------*/ -.loader{ - margin-top: calc(50vh - 44px); - margin-left: calc(50vw - 80px); -} - -.wave-loader { - animation: wave-loader 2s linear infinite; -} - -@keyframes wave-loader { - 0% { - width: 0px; - } - 50% { - width: 14px; - } - 100% { - width: 0px; - } -} - .expandable-right-pane{ transform: scaleX(0); transform-origin: right; diff --git a/packages/ui-kit/src/ui-kit.ts b/packages/ui-kit/src/ui-kit.ts index b8f1cac7d..a66e56ad1 100644 --- a/packages/ui-kit/src/ui-kit.ts +++ b/packages/ui-kit/src/ui-kit.ts @@ -29,6 +29,7 @@ export type { IModal } from './components/modal/interfaces/Modal.interface'; export { default as SplitView } from './components/split-view/SplitView'; export { default as Tabs } from './components/tabs/Tabs'; +export { default as TabsV3 } from './components/tabs/v3/Tabs'; export { default as Count } from './components/tabs/Count'; export { default as SecondaryTab } from './components/tabs/SecondaryTab'; @@ -107,6 +108,10 @@ export { default as Response } from './components/response/Response'; //----------------------------------------------------enums--------------------------------------------------------------------------- export { EPlacementForActive as ETabsPlacementForActive } from './components/tabs/interfaces/Tabs.interfaces'; +export type { + ITab, + EActiveBorderPosition, +} from './components/tabs/v3/Tab.interface'; export { EPopoverPosition } from './components/popover/interfaces/Popover.interfaces';