Skip to content

Commit

Permalink
Merge pull request #22 from PatrickMurrell/feat/addSaveAndLoadAsMulti…
Browse files Browse the repository at this point in the history
…pleFiles

Add Save And Load As Multiple Files Feature
  • Loading branch information
NickRimmer authored Mar 10, 2023
2 parents c366259 + b81abdf commit 68f10b4
Show file tree
Hide file tree
Showing 19 changed files with 426 additions and 209 deletions.
17 changes: 14 additions & 3 deletions src/components/configuration-dialog.styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@
}

&.form-control {
.auto-save-checkbox {
.input-label-with-error {
display: flex;
justify-content: space-between;
}

.checkbox {
display: flex;
gap: 4px;
align-items: center;
color: var(--hl);

Expand All @@ -18,11 +24,16 @@
margin-left: var(--padding-sm);
}
}

.checkboxes {
display: flex;
gap: 10px;
}
}

.error-message {
color: #ff8b00;
padding: 4px 0 10px;
padding: 0;

&.hide {
display: none;
Expand Down Expand Up @@ -58,6 +69,6 @@
}

.modal__content {
padding-bottom: 20px;
padding-bottom: 0;
}
}
60 changes: 42 additions & 18 deletions src/components/configuration-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import './configuration-dialog.styles.scss'
import React, { FC, useEffect, useState } from 'react'
import { ConfigurationDialogProps } from './configuration-dialog.types'
import { validatePath } from '../services/file-service'
import { isValidPath } from '../services/file-service'
import { DataService } from '../services/data-service'
import { Button } from './shared/button'
import {
PluginConfiguration,
PluginConfigurationDefault,
PluginModelsConfiguration,
} from '../services/configuration-service.types'
} from '../services/data-service.types'
import { showDialogSelectDirectoryAsync, showDialogSelectFileAsync } from '../insomnia/tools/dialogs'

export const ConfigurationDialog: FC<ConfigurationDialogProps> = ({workspaceId, context}) => {
const [isFilePathInputWrong, setIsFilePathInputWrong] = useState(true)
const [configuration, _setConfiguration] = useState<PluginConfiguration>(PluginConfigurationDefault)

const configurationService = new DataService(workspaceId, context.store)
const validateFilePathInput = (path: string | null | undefined) => !path || validatePath(path)
const validateFilePathInput =
(path: string | null | undefined) => !path ||
isValidPath(path, configuration.saveAsSingleFile)

// on mount
useEffect(() => {
Expand All @@ -26,6 +29,10 @@ export const ConfigurationDialog: FC<ConfigurationDialogProps> = ({workspaceId,
})
}, [])

useEffect(() => {
setIsFilePathInputWrong(validateFilePathInput(configuration.filePath))
}, [configuration])

// methods
const setConfigurationAsync = async (updates: Partial<PluginConfiguration>): Promise<void> => {
const result = {...configuration, ...updates}
Expand All @@ -39,15 +46,18 @@ export const ConfigurationDialog: FC<ConfigurationDialogProps> = ({workspaceId,
}

const onSelectFileClickedAsync = async (): Promise<void> => {
let path = await context.app.showSaveDialog()
const path = configuration.saveAsSingleFile
? await showDialogSelectFileAsync('json')
: await showDialogSelectDirectoryAsync('Workspace directory')

if (!path) return
if (!path?.toLocaleLowerCase().endsWith('.json')) path += '.json'

await onPathInputChangeAsync(path)
}

const onAutoSaveChangedAsync = (checked: boolean) => setConfigurationAsync({autoSave: checked})
const onSaveAsSingleFileChangedAsync = async (checked: boolean): Promise<void> => {
await setConfigurationAsync({saveAsSingleFile: checked})
}

const onModelsChangedAsync = (enabledModels: Partial<PluginModelsConfiguration>): Promise<void> => {
const result = {...configuration, enabledModels: {...configuration.enabledModels, ...enabledModels}}
Expand All @@ -56,24 +66,38 @@ export const ConfigurationDialog: FC<ConfigurationDialogProps> = ({workspaceId,

return (
<div className='form-control form-control--outlined plugin-free-sync configuration-dialog'>
<div>Configuration file path</div>
<div className={'input-label-with-error'}>
<div>Configuration file path</div>
{!isFilePathInputWrong && (<div className={`error-message`}>Path value is incorrect</div>)}
</div>


<div>
<input type={'text'} placeholder='Please, specify the path or select the file'
<input type={'text'} placeholder='Please, specify the path'
value={configuration.filePath || ''}
onChange={e => onPathInputChangeAsync(e.target.value)}/>
</div>

{!isFilePathInputWrong && (<div className={`error-message`}>Path value is incorrect</div>)}

<div className='buttons'>
<Button icon='fa-file' onClick={onSelectFileClickedAsync}>Select file...</Button>
<label className='auto-save-checkbox'>
Auto save on changes
<input type={'checkbox'}
disabled={true}
onChange={e => onAutoSaveChangedAsync(e.target.checked)}
checked={configuration.autoSave}/>
</label>
<Button icon='fa-file'
onClick={onSelectFileClickedAsync}>{configuration.saveAsSingleFile ? 'Select file...' : 'Select directory...'}</Button>

<div className='checkboxes'>
<label className='checkbox' title={'Save all settings in a single file'}>
<input type={'checkbox'}
onChange={e => onSaveAsSingleFileChangedAsync(e.target.checked)}
checked={configuration.saveAsSingleFile}/>
As a single file
</label>

<label className='checkbox' title={'Not available yet'}>
<input type={'checkbox'}
disabled={true}
onChange={e => onAutoSaveChangedAsync(e.target.checked)}
checked={configuration.autoSave}/>
Auto save
</label>
</div>
</div>

<hr className='pad-top'/>
Expand Down
9 changes: 7 additions & 2 deletions src/components/shared/button.styles.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
.plugin-free-sync {
&.btn-icon--left {
margin-right: .2em;

&.btn--super-duper-compact {
gap: 4px;
display: flex;
align-items: center;
padding-bottom: var(--padding-xs);
padding-top: var(--padding-xs);
}
}
21 changes: 21 additions & 0 deletions src/insomnia/tools/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,24 @@ export const showDialogComponent = ({context, title, children}: showDialogCompon
},
})
}

export const showDialogSelectDirectoryAsync = async (title: string): Promise<string | null> => {
const {canceled, filePaths} = await (window as any).dialog.showOpenDialog({
title,
buttonLabel: 'Select directory',
properties: ['openDirectory'],
})

if (canceled || !filePaths) return null

return filePaths[0]
}

export const showDialogSelectFileAsync = async (ext: string): Promise<string | null> => {
const {canceled, filePath} = await (window as any).dialog.showSaveDialog()

if (canceled || !filePath) return null

if (filePath.endsWith(`.${ext}`)) return filePath
return `${filePath}.${ext}`
}
5 changes: 0 additions & 5 deletions src/insomnia/types/workspace-raw.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/insomnia/types/workspace-raw.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { WorkspaceResource } from './workspace-resource.types'

export type WorkspaceRaw = {
resources: WorkspaceResource[]
}
5 changes: 0 additions & 5 deletions src/insomnia/types/workspace-resource.ts

This file was deleted.

9 changes: 9 additions & 0 deletions src/insomnia/types/workspace-resource.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type WorkspaceResource = {
_id: string,
_type: string,
parentId: string,
name?: string | null,
data?: object | null,
dataPropertyOrder?: object | null,
contents?: string | null,
}
9 changes: 7 additions & 2 deletions src/services/data-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PluginConfiguration, PluginConfigurationDefault } from './configuration-service.types'
import { PluginConfiguration, PluginConfigurationDefault } from './data-service.types'
import { InsomniaContextStore } from '../insomnia/types/context-store.types'

export class DataService {
Expand All @@ -18,7 +18,12 @@ export class DataService {
const value = await this._store.getItem(this._configurationStoreKey)
if (!value) return PluginConfigurationDefault

return JSON.parse(value) as PluginConfiguration
const result = JSON.parse(value) as PluginConfiguration

// old version configurations fix
result.saveAsSingleFile = result.saveAsSingleFile || result.saveAsSingleFile === undefined

return result
}

public async setConfigurationAsync(configuration: PluginConfiguration): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type PluginConfiguration = {
filePath: string,
autoSave: boolean,
saveAsSingleFile: boolean,
enabledModels: PluginModelsConfiguration
}

Expand All @@ -17,6 +18,7 @@ export type PluginModelsConfiguration = {
export const PluginConfigurationDefault: PluginConfiguration = {
filePath: '',
autoSave: false,
saveAsSingleFile: false,
enabledModels: {
apiSpec: true,
environmentBase: true,
Expand Down
24 changes: 22 additions & 2 deletions src/services/file-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
export const validatePath = (path: string | null | undefined): boolean =>
import { PathLike } from 'fs'
import { stat } from 'fs/promises'
import { extname } from 'path'

export const isValidPath = (path: string, asFile: boolean = true): boolean =>
path !== null &&
path !== undefined &&
path.length > 0 &&
validatePath(path) &&
(asFile ? !!extname(path) : !extname(path))

const validatePath = (path: string | null | undefined): boolean =>
path !== null &&
path !== undefined &&
path.length > 0 &&
new RegExp(
'^(?:[a-z]:)?[\\/\\\\]{0,2}(?:[.\\/\\\\ ](?![.\\/\\\\\\n])|[^<>:"|?*.\\/\\\\ \\n])+$', 'gmi')
.exec(path) !== null
.exec(path) !== null

export async function exists(f: PathLike) {
try {
await stat(f)
return true
} catch {
return false
}
}
Loading

0 comments on commit 68f10b4

Please sign in to comment.