Skip to content

Commit e983f90

Browse files
authored
feat(app): support editor preference (#18932)
* editors * update openerId to binary * types * work- around * refactor * wip * rejig actions * update gql fragments on front-end * update test * remove old code * lint * remove old test * wip * use i18n * use live mutation * save editor in same format as other preferences * update editor opening api * wip: types * types * lint * types * simplify types * implement prox ysettings * types * old code * old import * update test code * remove old code * allow using custom editor * add tests
1 parent 2eb0ff5 commit e983f90

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+815
-267
lines changed

packages/app/cypress/e2e/integration/settings.spec.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
describe('Settings', { viewportWidth: 1200 }, () => {
1+
describe('Settings', { viewportWidth: 600 }, () => {
22
beforeEach(() => {
33
cy.setupE2E('component-tests')
44

@@ -38,4 +38,18 @@ describe('Settings', { viewportWidth: 1200 }, () => {
3838
cy.findByText('Reconfigure Project').click()
3939
cy.wait('@ReconfigureProject')
4040
})
41+
42+
it('selects well known editor', () => {
43+
cy.visitApp()
44+
cy.get('[href="#/settings"]').click()
45+
cy.contains('Device Settings').click()
46+
cy.findByPlaceholderText('Custom path...').clear().type('/usr/local/bin/vim')
47+
48+
cy.intercept('POST', 'mutation-SetPreferredEditorBinary', (req) => {
49+
expect(req.body.variables).to.eql({ 'value': '/usr/local/bin/vim' })
50+
}).as('SetPreferred')
51+
52+
cy.get('[data-cy="use-custom-editor"]').click()
53+
cy.wait('@SetPreferred')
54+
})
4155
})

packages/app/src/settings/SettingsContainer.vue

+9-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
:icon="IconLaptop"
1010
max-height="800px"
1111
>
12-
<DeviceSettings />
12+
<ExternalEditorSettings :gql="props.gql" />
13+
<ProxySettings :gql="props.gql" />
14+
<TestingPreferences :gql="props.gql" />
1315
</SettingsCard>
1416
<SettingsCard
1517
:title="t('settingsPage.project.title')"
@@ -42,9 +44,11 @@
4244
import { useI18n } from '@cy/i18n'
4345
import { gql, useMutation } from '@urql/vue'
4446
import Button from '@cy/components/Button.vue'
47+
import ExternalEditorSettings from './device/ExternalEditorSettings.vue'
48+
import ProxySettings from './device/ProxySettings.vue'
4549
import SettingsCard from './SettingsCard.vue'
4650
import ProjectSettings from './project/ProjectSettings.vue'
47-
import DeviceSettings from './device/DeviceSettings.vue'
51+
import TestingPreferences from './device/TestingPreferences.vue'
4852
import { SettingsContainer_ReconfigureProjectDocument, SettingsContainerFragment } from '../generated/graphql'
4953
import IconLaptop from '~icons/cy/laptop_x24.svg'
5054
import IconFolder from '~icons/cy/folder-outline_x24.svg'
@@ -60,10 +64,13 @@ mutation SettingsContainer_ReconfigureProject {
6064
6165
gql`
6266
fragment SettingsContainer on Query {
67+
... TestingPreferences
6368
currentProject {
6469
id
6570
...ProjectSettings
6671
}
72+
...ExternalEditorSettings
73+
...ProxySettings
6774
}`
6875
6976
const props = defineProps<{

packages/app/src/settings/device/DeviceSettings.spec.tsx

-7
This file was deleted.

packages/app/src/settings/device/DeviceSettings.vue

-11
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,60 @@
11
import ExternalEditorSettings from './ExternalEditorSettings.vue'
22
import { defaultMessages } from '@cy/i18n'
3+
import { ExternalEditorSettingsFragmentDoc } from '../../generated/graphql-test'
34

45
const editorText = defaultMessages.settingsPage.editor
56

67
describe('<ExternalEditorSettings />', () => {
7-
beforeEach(() => {
8-
cy.mount(() => <ExternalEditorSettings class="p-12" />)
9-
})
10-
118
it('renders the placeholder by default', () => {
9+
cy.mountFragment(ExternalEditorSettingsFragmentDoc, {
10+
render: (gqlVal) => {
11+
return <ExternalEditorSettings gql={gqlVal} />
12+
},
13+
})
14+
1215
cy.findByText(editorText.noEditorSelectedPlaceholder).should('be.visible')
1316
})
1417

1518
it('renders the title and description', () => {
19+
cy.mountFragment(ExternalEditorSettingsFragmentDoc, {
20+
render: (gqlVal) => {
21+
return <ExternalEditorSettings gql={gqlVal} />
22+
},
23+
})
24+
1625
cy.findByText(editorText.description).should('be.visible')
1726
cy.findByText(editorText.title).should('be.visible')
1827
})
1928

2029
it('can select an editor', () => {
30+
cy.mountFragment(ExternalEditorSettingsFragmentDoc, {
31+
render: (gqlVal) => {
32+
return <ExternalEditorSettings gql={gqlVal} />
33+
},
34+
})
35+
2136
const optionsSelector = '[role=option]'
2237
const inputSelector = '[aria-haspopup=true]'
2338

2439
cy.get(inputSelector).click()
2540
.get(optionsSelector).should('be.visible')
2641
.get(optionsSelector).then(($options) => {
27-
const text = $options.first().text()
28-
2942
cy.wrap($options.first()).click()
30-
.get(optionsSelector).should('not.exist')
31-
.get(inputSelector).should('have.text', text)
3243
})
3344
})
45+
46+
it('can input a custom binary', () => {
47+
cy.mountFragment(ExternalEditorSettingsFragmentDoc, {
48+
render: (gqlVal) => {
49+
return <ExternalEditorSettings gql={gqlVal} />
50+
},
51+
})
52+
53+
cy.findByPlaceholderText('Custom path...').type('/usr/bin')
54+
cy.get('[data-cy="use-custom-editor"]').as('custom')
55+
cy.get('@custom').click()
56+
57+
cy.get('@custom').should('be.focused')
58+
cy.get('[data-cy="use-well-known-editor"]').should('not.be.focused')
59+
})
3460
})

packages/app/src/settings/device/ExternalEditorSettings.vue

+156-62
Original file line numberDiff line numberDiff line change
@@ -6,84 +6,178 @@
66
<template #description>
77
{{ t('settingsPage.editor.description') }}
88
</template>
9-
<Select
10-
v-model="selectedEditor"
11-
:options="externalEditors"
12-
item-value="name"
13-
:placeholder="t('settingsPage.editor.noEditorSelectedPlaceholder')"
14-
class="w-300px"
15-
>
16-
<template #input-prefix="{ value }">
17-
<Icon
18-
v-if="value"
19-
:icon="value.icon"
20-
class="text-md"
21-
/>
22-
<Icon
23-
v-else
24-
:icon="IconTerminal"
25-
class="text-gray-600 text-md"
26-
/>
27-
</template>
28-
<template #item-prefix="{value}">
29-
<Icon
30-
:icon="value.icon"
31-
class="text-md"
32-
/>
33-
</template>
34-
</Select>
9+
10+
<div class="flex items-center">
11+
<input
12+
v-model="editorToUse"
13+
type="radio"
14+
class="mr-5px"
15+
data-cy="use-well-known-editor"
16+
value="found"
17+
>
18+
19+
<Select
20+
:model-value="selectedWellKnownEditor"
21+
:options="externalEditors"
22+
item-value="name"
23+
item-key="id"
24+
:placeholder="t('settingsPage.editor.noEditorSelectedPlaceholder')"
25+
class="w-400px"
26+
@update:model-value="updateEditor"
27+
>
28+
<template #input-prefix="{ value }">
29+
<Icon
30+
v-if="value"
31+
:icon="icons[value.id]"
32+
class="text-md"
33+
/>
34+
<Icon
35+
v-else
36+
:icon="IconTerminal"
37+
class="text-gray-600 text-md"
38+
/>
39+
</template>
40+
<template #item-prefix="{ value }">
41+
<Icon
42+
:icon="value.icon"
43+
class="text-md"
44+
/>
45+
</template>
46+
</Select>
47+
</div>
48+
49+
<div class="flex items-center py-2">
50+
<input
51+
v-model="editorToUse"
52+
type="radio"
53+
class="mr-5px"
54+
value="custom"
55+
data-cy="use-custom-editor"
56+
>
57+
58+
<div class="w-400px">
59+
<Input
60+
v-model="customBinary"
61+
input-classes="text-sm"
62+
placeholder="Custom path..."
63+
@blur="setCustomBinary"
64+
>
65+
<template #prefix>
66+
<Icon
67+
:icon="IconTerminal"
68+
class="text-gray-600 text-md"
69+
/>
70+
</template>
71+
</Input>
72+
</div>
73+
</div>
3574
</SettingsSection>
3675
</template>
3776

3877
<script lang="ts" setup>
39-
40-
import { ref } from 'vue'
78+
import { ref, computed, watch, FunctionalComponent, SVGAttributes } from 'vue'
4179
import Icon from '@cy/components/Icon.vue'
4280
import SettingsSection from '../SettingsSection.vue'
4381
import { useI18n } from '@cy/i18n'
4482
import Select from '@cy/components/Select.vue'
83+
import Input from '@cy/components/Input.vue'
4584
import VSCode from '~icons/logos/visual-studio-code'
4685
import Atom from '~icons/logos/atom-icon'
4786
import Webstorm from '~icons/logos/webstorm'
4887
import Vim from '~icons/logos/vim'
4988
import Sublime from '~icons/logos/sublimetext-icon'
5089
import Emacs from '~icons/logos/emacs'
5190
import IconTerminal from '~icons/mdi/terminal'
91+
import { gql } from '@urql/core'
92+
import { SetPreferredEditorBinaryDocument, ExternalEditorSettingsFragment } from '../../generated/graphql'
93+
import { useMutation } from '@urql/vue'
94+
95+
// @ts-ignore (lachlan): add all icons for all editors such as RubyMine, etc
96+
const icons: Record<string, FunctionalComponent<SVGAttributes, {}>> = {
97+
'code': VSCode,
98+
'webstorm': Webstorm,
99+
'atom': Atom,
100+
'sublimetext': Sublime,
101+
'sublimetext2': Sublime,
102+
'sublimetextdev': Sublime,
103+
'vim': Vim,
104+
'emacs': Emacs,
105+
}
106+
107+
const externalEditors = computed(() => {
108+
return props.gql.localSettings.availableEditors?.map((x) => ({ ...x, icon: icons[x.id] })) || []
109+
})
110+
111+
gql`
112+
mutation SetPreferredEditorBinary ($value: String!) {
113+
setPreferredEditorBinary (value: $value)
114+
}`
115+
116+
gql`
117+
fragment ExternalEditorSettings on Query {
118+
localSettings {
119+
availableEditors {
120+
id
121+
name
122+
binary
123+
}
124+
125+
preferences {
126+
preferredEditorBinary
127+
}
128+
}
129+
}`
130+
131+
const setPreferredEditor = useMutation(SetPreferredEditorBinaryDocument)
52132
53-
// TODO, grab these from gql or the user's machine.
54-
const externalEditors = [
55-
{
56-
name: 'Visual Studio Code',
57-
key: 'vscode',
58-
icon: VSCode,
59-
},
60-
{
61-
name: 'Webstorm',
62-
key: 'webstorm',
63-
icon: Webstorm,
64-
},
65-
{
66-
name: 'Atom',
67-
key: 'atom',
68-
icon: Atom,
69-
},
70-
{
71-
name: 'Sublime Text',
72-
key: 'sublime',
73-
icon: Sublime,
74-
},
75-
{
76-
name: 'Vim',
77-
key: 'vim',
78-
icon: Vim,
79-
},
80-
{
81-
name: 'Emacs',
82-
key: 'emacs',
83-
icon: Emacs,
84-
},
85-
]
133+
const props = defineProps<{
134+
gql: ExternalEditorSettingsFragment
135+
}>()
86136
87137
const { t } = useI18n()
88-
const selectedEditor = ref<Record<string, any>>()
138+
139+
type Editor = ExternalEditorSettingsFragment['localSettings']['availableEditors'][number]
140+
141+
const customBinary = ref<string>('')
142+
const selectedWellKnownEditor = ref<Editor>()
143+
const editorToUse = ref<'found' | 'custom'>('found')
144+
145+
watch(
146+
() => props.gql.localSettings.preferences.preferredEditorBinary,
147+
(perferredEditorBinary) => {
148+
const isWellKnownEditor = props.gql.localSettings.availableEditors.find((x) => {
149+
return x.binary === perferredEditorBinary
150+
})
151+
152+
editorToUse.value = isWellKnownEditor ? 'found' : 'custom'
153+
154+
if (isWellKnownEditor) {
155+
selectedWellKnownEditor.value = isWellKnownEditor
156+
}
157+
158+
if (editorToUse.value === 'custom' && perferredEditorBinary) {
159+
customBinary.value = perferredEditorBinary
160+
}
161+
}, { immediate: true },
162+
)
163+
164+
watch(editorToUse, (val) => {
165+
if (val === 'custom') {
166+
setPreferredEditor.executeMutation({ value: customBinary.value })
167+
}
168+
})
169+
170+
const setCustomBinary = () => {
171+
if (editorToUse.value === 'custom') {
172+
setPreferredEditor.executeMutation({ value: customBinary.value })
173+
}
174+
}
175+
176+
const updateEditor = (editor: Editor) => {
177+
if (editorToUse.value !== 'found') {
178+
editorToUse.value = 'found'
179+
}
180+
181+
setPreferredEditor.executeMutation({ value: editor.binary })
182+
}
89183
</script>

0 commit comments

Comments
 (0)