feat: popover to configure editor (#62452)

Co-authored-by: Felix Kling <felix@felix-kling.de>
This commit is contained in:
Michael Bahr 2024-05-23 16:36:58 +02:00 committed by GitHub
parent 7c15db348d
commit d8284e34fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 207 additions and 16 deletions

View File

@ -32,3 +32,7 @@ export function isWindowsPlatform(): boolean {
// Examples: 'Win32', 'Windows', 'Win64'
return typeof window !== 'undefined' && window.navigator.platform.includes('Win')
}
export function getPlatform(): 'windows' | 'mac' | 'linux' | 'other' {
return isWindowsPlatform() ? 'windows' : isMacPlatform() ? 'mac' : isLinuxPlatform() ? 'linux' : 'other'
}

View File

@ -12,7 +12,12 @@ export { logger } from '@sourcegraph/common/src/util/logger'
export { isSafari } from '@sourcegraph/common/src/util/browserDetection'
export { isExternalLink, type LineOrPositionOrRange, SourcegraphURL } from '@sourcegraph/common/src/util/url'
export { parseJSONCOrError } from '@sourcegraph/common/src/util/jsonc'
export { isWindowsPlatform, isMacPlatform, isLinuxPlatform } from '@sourcegraph/common/src/util/browserDetection'
export {
isWindowsPlatform,
isMacPlatform,
isLinuxPlatform,
getPlatform,
} from '@sourcegraph/common/src/util/browserDetection'
let highlightingLoaded = false

View File

@ -1,14 +1,25 @@
<script lang="ts">
import { getEditor, parseBrowserRepoURL, buildRepoBaseNameAndPath, buildEditorUrl } from '$lib/web'
import {
getEditor,
parseBrowserRepoURL,
buildRepoBaseNameAndPath,
buildEditorUrl,
isProjectPathValid,
} from '$lib/web'
import { getEditorSettingsErrorMessage } from './build-url'
import Tooltip from '$lib/Tooltip.svelte'
import EditorIcon from '$lib/repo/open-in-editor/EditorIcon.svelte'
import { settings } from '$lib/stores'
import { page } from '$app/stores'
import type { ExternalRepository } from '$lib/graphql-types'
import type { ExternalRepository, SettingsEdit } from '$lib/graphql-types'
import DefaultEditorIcon from '$lib/repo/open-in-editor/DefaultEditorIcon.svelte'
import Popover from '$lib/Popover.svelte';
import { Button } from '$lib/wildcard';
import { supportedEditors } from '$lib/web';
import { getPlatform } from '$lib/common';
export let externalServiceType: ExternalRepository['serviceType'] = ''
export let updateUserSetting: (edit: SettingsEdit) => Promise<void>;
$: openInEditor = $settings?.openInEditor
@ -20,6 +31,48 @@
$: ({ repoName, filePath, position, range } = parseBrowserRepoURL($page.url.toString()))
$: start = position ?? range?.start
$: defaultProjectPath = ''
$: selectedEditorId = undefined
$: areSettingsValid = !!selectedEditorId && isProjectPathValid(defaultProjectPath);
let isSaving = false;
$: handleEditorUpdate = async (): Promise<void> => {
if (!selectedEditorId || !defaultProjectPath) {
return;
}
isSaving = true;
try {
await updateUserSetting({
value: defaultProjectPath,
keyPath: [{property: 'openInEditor'}, {property: 'projectPaths.default'}],
});
await updateUserSetting({
value: [selectedEditorId],
keyPath: [{property: 'openInEditor'}, {property: 'editorIds'}],
});
openInEditor = {
editorIds: [selectedEditorId],
'projectPaths.default': defaultProjectPath,
}
} finally {
isSaving = false;
}
}
function getSystemAwareProjectPathExample(suffix?: string) {
switch (getPlatform()) {
case 'windows':
return 'C:\\Users\\username\\Projects' + (suffix ? `\\${suffix}` : '');
case 'linux':
return '/home/username/Projects' + (suffix ? `/${suffix}` : '');
case 'mac':
default:
return '/Users/username/Projects' + (suffix ? `/${suffix}` : '');
}
}
</script>
{#if editors}
@ -27,6 +80,7 @@
{#if editor}
<Tooltip tooltip={`Open in ${editor.name}`}>
<a
class="action-href"
href={buildEditorUrl(
buildRepoBaseNameAndPath(repoName, externalServiceType, filePath),
start,
@ -37,23 +91,72 @@
target="_blank"
rel="noopener noreferrer"
>
<EditorIcon editorId={editor.id} />
<EditorIcon editorId={editor.id}/>
<span data-action-label> Editor </span>
</a>
</Tooltip>
{/if}
{/each}
{:else if editorSettingsErrorMessage}
<Tooltip tooltip={editorSettingsErrorMessage}>
<a href="/help/integration/open_in_editor" target="_blank">
<DefaultEditorIcon />
<span data-action-label> Editor </span>
</a>
</Tooltip>
<Popover let:registerTrigger let:toggle placement="left-start">
<Tooltip tooltip="Set your preferred editor">
<span use:registerTrigger on:click={() => toggle()}>
<DefaultEditorIcon/>
<span data-action-label> Editor </span>
</span>
</Tooltip>
<div slot="content" class="open-in-editor-popover">
<form on:submit={handleEditorUpdate} novalidate>
<h3>Set your preferred editor</h3>
<p>
Open this and other files directly in your editor. Set your path and editor to get started. Update
any time in your user settings.
</p>
<label>
Default projects path
<input
id="OpenInEditorForm-projectPath"
type="text"
name="projectPath"
placeholder="/Users/username/projects"
required
autocorrect="off"
autocapitalize="off"
spellcheck={false}
bind:value={defaultProjectPath}
class="form-input"
/>
</label>
<p class="small form-info">
The directory that contains your repository checkouts. For example, if this repository is
checked out to <code>{`${getSystemAwareProjectPathExample('cody')}`}</code>, then set your default projects path
to <code>{getSystemAwareProjectPathExample()}</code>.
</p>
<label>
Editor
<select class="form-input" id="OpenInEditorForm-editor" bind:value={selectedEditorId}>
<option value=""></option>
{#each supportedEditors.sort((a, b) => a.name.localeCompare(b.name)).filter(editor => editor.id !== 'custom') as editor}
<option value={editor.id}>{editor.name}</option>
{/each}
</select>
</label>
<p class="small form-info">Use a different editor?{' '}
<a href="/help/integration/open_in_editor" target="_blank" rel="noreferrer noopener">Set up a
different editor</a>
</p>
<Button variant="primary" type="submit" disabled={!areSettingsValid || isSaving}>
Save
</Button>
</form>
</div>
</Popover>
{/if}
<style lang="scss">
a {
.action-href {
display: flex;
align-items: center;
justify-content: center;
@ -66,4 +169,27 @@
color: var(--text-title);
}
}
.open-in-editor-popover {
isolation: isolate;
width: 25rem;
padding: 1.25rem 1rem;
background-color: var(--color-bg-1);
}
.form-label {
font-weight: 500;
}
.form-input {
width: 100%;
padding: 0.5rem;
border-radius: 0.25rem;
border: 1px solid var(--border-color);
}
.form-info {
margin-top: 0.5rem;
}
</style>

View File

@ -2,10 +2,18 @@ import { error, redirect } from '@sveltejs/kit'
import { isErrorLike, parseJSONCOrError } from '$lib/common'
import { getGraphQLClient } from '$lib/graphql'
import type { SettingsEdit } from '$lib/graphql-types'
import type { Settings } from '$lib/shared'
import type { LayoutLoad } from './$types'
import { Init, EvaluatedFeatureFlagsQuery, GlobalAlertsSiteFlags, DisableSveltePrototype } from './layout.gql'
import {
Init,
EvaluatedFeatureFlagsQuery,
GlobalAlertsSiteFlags,
DisableSveltePrototype,
EditSettings,
LatestSettingsQuery,
} from './layout.gql'
import { dotcomMainNavigation, mainNavigation } from './navigation'
// Disable server side rendering for the whole app
@ -62,5 +70,32 @@ export const load: LayoutLoad = async ({ fetch }) => {
throw new Error(`Failed to disable svelte feature flags: ${result.error}`)
}
},
updateUserSetting: async (edit: SettingsEdit): Promise<void> => {
// We have to set network-only here, because otherwise the client will reuse a previously cached value
const latestSettings = await client.query(LatestSettingsQuery, {}, { requestPolicy: 'network-only', fetch })
if (!latestSettings.data || latestSettings.error) {
throw new Error(`Failed to fetch latest settings during editor update: ${latestSettings.error}`)
}
const userSetting = latestSettings.data.viewerSettings.subjects.find(s => s.__typename === 'User')
if (!userSetting) {
throw new Error('Failed to find user settings subject')
}
const lastID = userSetting.latestSettings?.id
if (!lastID) {
throw new Error('Failed to get new last ID from settings result')
}
const mutationResult = await client.mutation(
EditSettings,
{
lastID,
subject: userSetting.id,
edit,
},
{ requestPolicy: 'network-only', fetch }
)
if (!mutationResult.data || mutationResult.error) {
throw new Error(`Failed to update editor path: ${mutationResult.error}`)
}
},
}
}

View File

@ -1,6 +1,6 @@
import { BehaviorSubject, concatMap, from, map } from 'rxjs'
import { fetchBlameHunksMemoized, type BlameHunkData } from '@sourcegraph/web/src/repo/blame/shared'
import { type BlameHunkData, fetchBlameHunksMemoized } from '@sourcegraph/web/src/repo/blame/shared'
import { SourcegraphURL } from '$lib/common'
import { getGraphQLClient, mapOrThrow } from '$lib/graphql'
@ -11,9 +11,9 @@ import { assertNonNullable } from '$lib/utils'
import type { PageLoad, PageLoadEvent } from './$types'
import {
BlobDiffViewCommitQuery,
BlobFileViewHighlightedFileQuery,
BlobFileViewCommitQuery_revisionOverride,
BlobFileViewBlobQuery,
BlobFileViewCommitQuery_revisionOverride,
BlobFileViewHighlightedFileQuery,
} from './page.gql'
function loadDiffView({ params, url }: PageLoadEvent) {

View File

@ -156,7 +156,7 @@
<svelte:fragment slot="actions">
{#await data.externalServiceType then externalServiceType}
{#if externalServiceType && !isBinaryFile}
<OpenInEditor {externalServiceType} />
<OpenInEditor {externalServiceType} updateUserSetting={data.updateUserSetting} />
{/if}
{/await}
{#if blob}

View File

@ -42,3 +42,24 @@ fragment AuthenticatedUser on User {
...GlobalNavigation_User
...SearchInput_AuthenticatedUser
}
mutation EditSettings($subject: ID!, $lastID: Int, $edit: SettingsEdit!) {
settingsMutation(input: { subject: $subject, lastID: $lastID }) {
editSettings(edit: $edit) {
empty {
alwaysNil
}
}
}
}
query LatestSettingsQuery {
viewerSettings {
subjects {
id
latestSettings {
id
}
}
}
}