Skip to content

Commit

Permalink
Merge pull request #16 from NickRimmer/features/workspace-tabs-save
Browse files Browse the repository at this point in the history
Features/workspace tabs save
  • Loading branch information
NickRimmer authored Jul 18, 2023
2 parents 0c372f1 + 75e6bc3 commit 90a715c
Show file tree
Hide file tree
Showing 20 changed files with 422 additions and 224 deletions.
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"@types/nedb": "^1.8.13",
"copyfiles": "^2.4.1",
"esbuild": "^0.18.11",
"esbuild-runner": "^2.2.2",
Expand Down
18 changes: 18 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { WindowApp, WindowMain } from './insomnia/insomnia.types'

declare global {
interface Window {
app: WindowApp,
main: WindowMain
}
}

type WindowApp = {
getAppPath: () => string,
getPath: (name: string) => string,
}

type WindowMain = {
on(channelName: string, callback: (e: any, data: any) => void): (() => void),
}
12 changes: 7 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import * as insomnia from './services/insomnia/connector'
import * as ui from './ui'
import { cleanupWorkspacesAsync } from './services/db'

const init = () => {
const initAsync = async () => {
// can be used during development, to be able to access insomnia instance from console for experiments
// (global as any).dev = {
// insomnia
// }

// initialize ui components
ui.render()
await cleanupWorkspacesAsync()
console.log('[plugin-navigator]', 'initialized')
}

let tries = 0
const waitForConnection = () => {
const waitForConnectionAsync = async () => {
if (!insomnia.connect()) {
if (tries++ < 25) window.setTimeout(waitForConnection, 200)
if (tries++ < 25) window.setTimeout(waitForConnectionAsync, 200)
else console.error('[plugin-navigator]', 'cannot connect to Insomnia')
} else init()
} else await initAsync()
}

waitForConnection()
waitForConnectionAsync()
14 changes: 14 additions & 0 deletions src/services/db/cleanup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { database } from './db'
import { getState } from '../insomnia/connector/index'

export const cleanupWorkspacesAsync = async () => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const existsWorkspaces = Object.values(getState().workspaces).map((x: any) => x._id)
const storedWorkspaces = await database.getAllData()
const removeWorkspaces = storedWorkspaces.filter(x => !existsWorkspaces.includes(x.workspaceId))
await Promise.all(removeWorkspaces.map(x => database.remove(x)))
} catch (err) {
console.error('[plugin-navigator]', 'cleanupWorkspaces', err)
}
}
9 changes: 9 additions & 0 deletions src/services/db/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Datastore from 'nedb'
import * as path from 'path'

const NeDb = window.require('nedb')
const dbName = 'Plugin.request-navigator'
const dbPath = path.join(window.app.getPath('userData'), `insomnia.${dbName}.db`)
const databaseInternal = new NeDb({ filename: dbPath, autoload: true, corruptAlertThreshold: 0.9, inMemoryOnly: false }) as Datastore

export const database = databaseInternal
10 changes: 10 additions & 0 deletions src/services/db/db.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type WorkspaceTab = {
requestId: string,
index: number,
isActive?: boolean,
}

export type Workspace = {
workspaceId: string,
tabs: WorkspaceTab[],
}
3 changes: 3 additions & 0 deletions src/services/db/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './db'
export * from './db.types'
export * from './cleanup'
1 change: 1 addition & 0 deletions src/services/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './insomnia-requests'
12 changes: 12 additions & 0 deletions src/services/helpers/insomnia-requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { DocBaseModel } from '../insomnia/types'

export const getRequestMethodName = (requestInfo: DocBaseModel) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let method = (requestInfo as any).method
if (!method) {
if (requestInfo.type === 'GrpcRequest') method = 'gRPC'
else method = 'N/A'
}

return method
}
1 change: 0 additions & 1 deletion src/services/insomnia/connector/refs-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { isCurrentConnectionStillActive } from './refs-common'

export type ReactRefs = {
//TODO add types
store: any;
router: any;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ui/tab-button/tab-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const TabButton: FC<TabButtonProps> = (props) => {
<i className='fa-solid fa-xmark'></i>
</div>
<div className='title'>
<span className={`method ${method.toLocaleLowerCase()}`}>{method.toUpperCase()}</span>
{method && <span className={`method ${method.toLocaleLowerCase()}`}>{method.toUpperCase()}</span>}
<span>{children}</span>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/ui/tab-button/tab-button.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ export type TabButtonProps = {
isHidden?: boolean,
requestId: string,
title: string,
method: string,
method: string | undefined,
}
48 changes: 48 additions & 0 deletions src/ui/tabs-panel/tabs-panel.hook.contextmenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { TabData } from './tabs-panel.types'

export const useContextMenu = ({ tabs, setTabs, showTabContent }: {
tabs: TabData[],
setTabs: (tabs: TabData[]) => void,
showTabContent: (requestId: string) => void,
}) => {

const onCloseOthersClicked = (requestId: string): void => {
const tabData = tabs.find(tab => tab.requestId == requestId)
if (!tabData) {
console.error('tab not found', requestId)
return
}

tabData.isActive = true
const updated = tabs.filter((tab) => tab.requestId === tabData.requestId)
setTabs(updated)
showTabContent(tabData.requestId)
}

const onClickCloseOnRight = (requestId: string): void => {
const tabData = tabs.find(tab => tab.requestId == requestId)
if (!tabData) {
console.error('tab not found', requestId)
return
}

const closedTabIndex = tabs.findIndex((tab) => tab.requestId === tabData.requestId)
const updated = tabs.slice(0, closedTabIndex + 1)

const currentActive = updated.findIndex((tab) => tab.isActive)
if (currentActive == -1) tabData.isActive = true

setTabs(updated)
showTabContent(tabData.requestId)
}

const onClickCloseAll = (): void => {
setTabs([])
}

return {
onCloseOthersClicked,
onClickCloseOnRight,
onClickCloseAll,
}
}
57 changes: 57 additions & 0 deletions src/ui/tabs-panel/tabs-panel.hook.dropdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useEffect } from 'react'
import { isCurrentConnectionStillActive } from '../../services/insomnia/connector/refs-common'
import { TabData } from './tabs-panel.types'

export const useDropdown = ({ tabs, id }: { tabs: TabData[], id: string }) => {
const [screenSize, setScreenSize] = React.useState<number>(0)
const [collapsedTabs, setCollapsedTabs] = React.useState<TabData[]>([])

useEffect(() => {
const handleResize = () => {
if (!isCurrentConnectionStillActive()) {
window.removeEventListener('resize', handleResize)
return
}
setScreenSize(window.innerWidth)
}
window.addEventListener('resize', handleResize)
}, [])

// when tabs changed or screen size updated - update dropdown view
useEffect(() => {
const root = document.getElementById(id)
const parent = root?.querySelector('.items')
if (!parent) {
return
}

const children = parent.querySelectorAll('.plugin-request-navigator-tab-button')
if (!children?.length) {
return
}

const parentRightBound = parent.getBoundingClientRect().right
if (!parentRightBound) {
console.error('[plugin-navigator]', 'parent has no right bound')
return
}

const tabsToCollapse: TabData[] = []
children.forEach((element) => {
if (element.getBoundingClientRect().right > parentRightBound) {
tabsToCollapse.push({
method: element.getAttribute('data-method'),
title: element.getAttribute('data-title'),
requestId: element.getAttribute('data-request-id'),
isActive: element.classList.contains('active')
} as TabData)
}
})

setCollapsedTabs(tabsToCollapse)
}, [tabs, screenSize])

return {
collapsedTabs,
}
}
66 changes: 66 additions & 0 deletions src/ui/tabs-panel/tabs-panel.hook.request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { MutableRefObject, useEffect } from 'react'
import { TabData } from './tabs-panel.types'
import { onRequestSelected } from '../../services/insomnia/events/request-selected'
import { onRequestUpdated } from '../../services/insomnia/events/request-updated'
import { onRequestDeleted } from '../../services/insomnia/events/request-deleted'
import { getAllRequests } from '../../services/insomnia/connector'
import { getRequestMethodName } from '../../services/helpers'

export const useRequestHandlers = ({ setTabs, tabDataRef }: { setTabs: (tabs: TabData[]) => void, tabDataRef: MutableRefObject<TabData[]> }) => {
// when request selected - add or activate tab
useEffect(() => {
onRequestSelected((doc) => {
const requestId = (doc as any).activeRequestId
if (!requestId) {
console.warn('onRequestSelected', 'unexpected doc, activeRequestId not found', doc)
return
}

if (!tabDataRef.current.find(tab => tab.requestId == requestId)) {
const requestInfo = getAllRequests()[requestId]

const method = getRequestMethodName(requestInfo)
const tabData = { isActive: true, requestId, title: requestInfo.name, method }
tabDataRef.current.forEach((x) => x.isActive = false)
setTabs([...tabDataRef.current, tabData])
} else {
tabDataRef.current.forEach((x) => x.isActive = x.requestId == requestId)
setTabs([...tabDataRef.current])
}
})
}, [])

// when request renamed - renamed tab
useEffect(() => {
onRequestUpdated((doc) => {
const requestId = doc._id
if (!requestId) {
console.warn('onRequestUpdated', 'unexpected doc, request id not found', doc)
return
}

const updatedList = [...tabDataRef.current]
const tab = updatedList.find(tab => tab.requestId == requestId)
if (!tab) return

if (tab.title == doc.name) return
tab.title = doc.name
setTabs(updatedList)
})
}, [])

// when request removed - remove tab
useEffect(() => {
onRequestDeleted((doc) => {
const requestId = doc._id
if (!requestId) {
console.warn('onRequestUpdated', 'unexpected doc, request id not found', doc)
return
}

const updatedList = [...tabDataRef.current].filter(tab => tab.requestId != requestId)
setTabs(updatedList)
})
}, [])
}
14 changes: 14 additions & 0 deletions src/ui/tabs-panel/tabs-panel.hook.sortable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TabData } from './tabs-panel.types'

export const useSortable = ({ tabs, setTabs }: { tabs: TabData[], setTabs: (tabs: TabData[]) => void }) => {
const onSortEnd = ({ oldIndex, newIndex }: { oldIndex: number, newIndex: number }): void => {
// move tab from old index to new index
const element = tabs.splice(oldIndex, 1)[0]
tabs.splice(newIndex, 0, element)
setTabs([...tabs])
}

return {
onSortEnd,
}
}
Loading

0 comments on commit 90a715c

Please sign in to comment.