Skip to content

Commit

Permalink
feat: import config (#55)
Browse files Browse the repository at this point in the history
* Adds ability to import a config file while creating a project.
* Adds a test for the config file importer.
* Updates config importer to use new select file. Updates test.
  • Loading branch information
cimigree authored Jan 8, 2025
1 parent 80b5fb5 commit 6a9ffae
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 66 deletions.
10 changes: 5 additions & 5 deletions messages/renderer/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,20 +212,20 @@
"screens.PrivacyPolicy.whyCollectedDescription": {
"message": "Crash data and app errors together with the device and app info provide Awana Digital with the information we need to fix errors and bugs in the app. The performance data helps us improve the responsiveness of the app and identify errors. User counts, including total users, users per country, and users per project, help justify ongoing investment in the development of CoMapeo."
},
"screens.ProjectCreationScreen.addName": {
"message": "Create Project"
},
"screens.ProjectCreationScreen.advancedProjectSettings": {
"message": "Advanced Project Settings"
},
"screens.ProjectCreationScreen.characterCount": {
"message": "{count}/{maxLength}"
},
"screens.ProjectCreationScreen.createProject": {
"message": "Create Project"
},
"screens.ProjectCreationScreen.enterNameLabel": {
"message": "Name your project"
},
"screens.ProjectCreationScreen.errorSavingProjectName": {
"message": "An error occurred while saving your project name. Please try again."
"screens.ProjectCreationScreen.errorSavingProject": {
"message": "An error occurred while saving your project. Please try again."
},
"screens.ProjectCreationScreen.importConfig": {
"message": "Import Config"
Expand Down
10 changes: 8 additions & 2 deletions src/renderer/src/hooks/mutations/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ export function useCreateProject() {

return useMutation({
mutationKey: [CREATE_PROJECT_KEY],
mutationFn: (name?: string) => {
return api.createProject({ name })
mutationFn: (opts?: Parameters<typeof api.createProject>[0]) => {
if (opts) {
return api.createProject(opts)
} else {
// Have to avoid passing `undefined` explicitly
// See https://github.com/digidem/comapeo-mobile/issues/392
return api.createProject()
}
},
onSuccess: () => {
queryClient.invalidateQueries({
Expand Down
70 changes: 70 additions & 0 deletions src/renderer/src/hooks/useConfigFileImporter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { useSelectProjectConfigFile } from './mutations/file-system'

describe('useSelectProjectConfigFile', () => {
beforeEach(() => {
window.runtime = {
getLocale: vi.fn().mockResolvedValue('en'),
updateLocale: vi.fn(),
selectFile: vi.fn(),
}
})

function createWrapper() {
const queryClient = new QueryClient()
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}

it('returns filename and filepath from window.runtime.selectFile', async () => {
vi.spyOn(window.runtime, 'selectFile').mockResolvedValue({
name: 'myFile.comapeocat',
path: '/Users/cindy/documents/myFile.comapeocat',
})

const { result } = renderHook(() => useSelectProjectConfigFile(), {
wrapper: createWrapper(),
})

await act(async () => {
const val = await result.current.mutateAsync(undefined)
expect(val).toEqual({
name: 'myFile.comapeocat',
path: '/Users/cindy/documents/myFile.comapeocat',
})
})
})

it('returns undefined if user cancels', async () => {
vi.spyOn(window.runtime, 'selectFile').mockResolvedValue(undefined)

const { result } = renderHook(() => useSelectProjectConfigFile(), {
wrapper: createWrapper(),
})

await act(async () => {
const val = await result.current.mutateAsync(undefined)
expect(val).toBeUndefined()
})
})

it('throws if the returned object has invalid shape', async () => {
vi.spyOn(window.runtime, 'selectFile').mockRejectedValue(
new Error('Value has invalid shape'),
)

const { result } = renderHook(() => useSelectProjectConfigFile(), {
wrapper: createWrapper(),
})

await expect(
act(async () => {
await result.current.mutateAsync()
}),
).rejects.toThrow('Value has invalid shape')
})
})
126 changes: 71 additions & 55 deletions src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { Text } from '../../components/Text'
import { PROJECT_NAME_MAX_LENGTH_GRAPHEMES } from '../../constants'
import { useActiveProjectIdStoreActions } from '../../contexts/ActiveProjectIdProvider'
import { useSelectProjectConfigFile } from '../../hooks/mutations/file-system'
import { useCreateProject } from '../../hooks/mutations/projects'
import ProjectImage from '../../images/add_square.png'

Expand All @@ -37,8 +38,8 @@ export const m = defineMessages({
id: 'screens.ProjectCreationScreen.placeholder',
defaultMessage: 'Project Name',
},
addName: {
id: 'screens.ProjectCreationScreen.addName',
createProject: {
id: 'screens.ProjectCreationScreen.createProject',
defaultMessage: 'Create Project',
},
characterCount: {
Expand All @@ -53,10 +54,10 @@ export const m = defineMessages({
id: 'screens.ProjectCreationScreen.importConfig',
defaultMessage: 'Import Config',
},
errorSavingProjectName: {
id: 'screens.ProjectCreationScreen.errorSavingProjectName',
errorSavingProject: {
id: 'screens.ProjectCreationScreen.errorSavingProject',
defaultMessage:
'An error occurred while saving your project name. Please try again.',
'An error occurred while saving your project. Please try again.',
},
saving: {
id: 'screens.ProjectCreationScreen.saving',
Expand Down Expand Up @@ -104,55 +105,79 @@ const HorizontalLine = styled('div')({
width: '65%',
})

const FileNameDisplay = styled(Text)({
textAlign: 'center',
marginTop: 12,
maxWidth: '100%',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
display: 'inline-block',
})

function CreateProjectScreenComponent() {
const navigate = useNavigate()
const { formatMessage } = useIntl()

const [projectName, setProjectName] = useState('')
const [error, setError] = useState(false)
const [hasNameError, setHasNameError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const setProjectNameMutation = useCreateProject()
const [configPath, setConfigPath] = useState<string | undefined>(undefined)
const [fileName, setFileName] = useState<string | undefined>()

const createProjectMutation = useCreateProject()
const selectConfigFile = useSelectProjectConfigFile()
const { setActiveProjectId } = useActiveProjectIdStoreActions()

const [configFileName, setConfigFileName] = useState<string | null>(null)
function handleImportConfig() {
selectConfigFile.mutate(undefined, {
onSuccess: (file) => {
if (file) {
setConfigPath(file.path)
setFileName(file.name)
}
},
onError: (err) => {
console.error('Error selecting file', err)
},
})
}

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
setProjectName(value)

const localError: boolean = checkForError(
value.trim(),
PROJECT_NAME_MAX_LENGTH_GRAPHEMES,
)
setProjectName(value)
if (localError !== error) {
setError(localError)
}
setError(localError)
setHasNameError(localError)
}

const graphemeCount = countGraphemes(projectName.trim())

const handleAddName = () => {
const handleCreateProject = () => {
if (checkForError(projectName, PROJECT_NAME_MAX_LENGTH_GRAPHEMES)) {
setError(true)
setHasNameError(true)
return
}
setProjectNameMutation.mutate(projectName, {
onSuccess: (projectId) => {
setActiveProjectId(projectId)
navigate({ to: '/tab1' })
},
onError: (error) => {
console.error('Error setting project name:', error)
setErrorMessage(formatMessage(m.errorSavingProjectName))
},
})
}

function importConfigFile() {
// Placeholder for file import logic
setConfigFileName('myProjectConfig.comapeocat')
createProjectMutation.mutate(
{ name: projectName.trim(), configPath },
{
onSuccess: (projectId) => {
setActiveProjectId(projectId)
navigate({ to: '/tab1' })
},
onError: (error) => {
console.error('Error saving project:', error)
setErrorMessage(formatMessage(m.errorSavingProject))
},
},
)
}

const backPressHandler = setProjectNameMutation.isPending
const backPressHandler = createProjectMutation.isPending
? undefined
: () => navigate({ to: '/Onboarding/CreateJoinProjectScreen' })

Expand All @@ -177,7 +202,7 @@ function CreateProjectScreenComponent() {
value={projectName}
onChange={handleChange}
variant="outlined"
error={error}
error={hasNameError}
sx={{
'& .MuiFormLabel-asterisk': {
color: 'red',
Expand All @@ -197,7 +222,7 @@ function CreateProjectScreenComponent() {
},
}}
/>
<CharacterCount error={error}>
<CharacterCount error={hasNameError}>
{formatMessage(m.characterCount, {
count: graphemeCount,
maxLength: PROJECT_NAME_MAX_LENGTH_GRAPHEMES,
Expand Down Expand Up @@ -243,41 +268,32 @@ function CreateProjectScreenComponent() {
maxWidth: 350,
padding: '12px 20px',
}}
onClick={importConfigFile}
onClick={handleImportConfig}
>
{formatMessage(m.importConfig)}
</Button>
{configFileName && (
<Text style={{ textAlign: 'center', marginTop: 12 }}>
{configFileName}
</Text>
{fileName && (
<FileNameDisplay data-testid="filename-display">
{fileName}
</FileNameDisplay>
)}
</AccordionDetails>
</Accordion>
</div>
</div>
<div
<Button
onClick={handleCreateProject}
style={{
marginTop: 12,
width: '100%',
display: 'flex',
justifyContent: 'center',
maxWidth: 350,
padding: '12px 20px',
}}
disabled={createProjectMutation.isPending}
>
<Button
onClick={handleAddName}
style={{
width: '100%',
maxWidth: 350,
padding: '12px 20px',
}}
disabled={setProjectNameMutation.isPending}
>
{setProjectNameMutation.isPending
? formatMessage(m.saving)
: formatMessage(m.addName)}
</Button>
</div>
{formatMessage(
createProjectMutation.isPending ? m.saving : m.createProject,
)}
</Button>
</OnboardingScreenLayout>
)
}
4 changes: 0 additions & 4 deletions src/renderer/src/routes/Onboarding/DeviceNamingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,3 @@ export function DeviceNamingScreenComponent() {
</OnboardingScreenLayout>
)
}

export function getUtf8ByteLength(text: string): number {
return new TextEncoder().encode(text).length
}

0 comments on commit 6a9ffae

Please sign in to comment.