Use SectionCard instead of ClickableCard

- commit grew a bit in scope
- lots of refactoring to settings pages
- clickable card dropped
This commit is contained in:
Mattias Granlund 2024-03-08 00:16:49 +01:00
parent 0311052f24
commit 645e076a78
15 changed files with 395 additions and 513 deletions

View File

@ -0,0 +1,12 @@
import { invoke } from './ipc';
export type GitCredentialCheck = {
error?: string;
ok: boolean;
};
export class AuthService {
async getPublicKey() {
return await invoke<string>('get_public_key');
}
}

View File

@ -16,13 +16,12 @@ import { get } from 'svelte/store';
import type { Project as CloudProject } from '$lib/backend/cloud';
import { goto } from '$app/navigation';
export type Key =
| 'default'
| 'generated'
| 'gitCredentialsHelper'
| {
local: { private_key_path: string; passphrase?: string };
};
export type KeyType = 'default' | 'generated' | 'gitCredentialsHelper' | 'local';
export type LocalKey = {
local: { private_key_path: string; passphrase?: string };
};
export type Key = Exclude<KeyType, 'local'> | LocalKey;
export type Project = {
id: string;

View File

@ -1,7 +1,7 @@
<script lang="ts">
import ClickableCard from './ClickableCard.svelte';
import InfoMessage from './InfoMessage.svelte';
import Link from './Link.svelte';
import SectionCard from './SectionCard.svelte';
import Toggle from './Toggle.svelte';
import { appErrorReportingEnabled, appMetricsEnabled } from '$lib/config/appSettings';
@ -39,7 +39,7 @@
</div>
<div class="analytics-settings__actions">
<ClickableCard on:click={toggleErrorReporting}>
<SectionCard on:click={toggleErrorReporting} orientation="row">
<svelte:fragment slot="title">Error reporting</svelte:fragment>
<svelte:fragment slot="body">
Toggle reporting of application crashes and errors.
@ -47,15 +47,15 @@
<svelte:fragment slot="actions">
<Toggle checked={$errorReportingEnabled} on:change={toggleErrorReporting} />
</svelte:fragment>
</ClickableCard>
</SectionCard>
<ClickableCard on:click={toggleMetrics}>
<SectionCard on:click={toggleMetrics} orientation="row">
<svelte:fragment slot="title">Usage metrics</svelte:fragment>
<svelte:fragment slot="body">Toggle sharing of usage statistics.</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle checked={$metricsEnabled} on:change={toggleMetrics} />
</svelte:fragment>
</ClickableCard>
</SectionCard>
{#if updatedTelemetrySettings}
<InfoMessage>Changes will take effect on the next application start.</InfoMessage>

View File

@ -1,119 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let padding: string = 'var(--space-16)';
export let disabled = false;
export let checked = false;
export let hasTopRadius = true;
export let hasBottomRadius = true;
export let hasBottomLine = true;
const SLOTS = $$props.$$slots;
const dispatchClick = createEventDispatcher<{
click: void;
}>();
const dispatchChange = createEventDispatcher<{
change: boolean;
}>();
</script>
<button
class="clickable-card"
class:has-top-radius={hasTopRadius}
class:has-bottom-radius={hasBottomRadius}
class:has-bottom-line={hasBottomLine}
style="padding: {padding}"
on:click={() => {
dispatchClick('click');
dispatchChange('change', checked);
checked = !checked;
}}
class:card-disabled={disabled}
{disabled}
>
<div class="clickable-card__content">
{#if SLOTS.title}
<h3 class="text-base-15 text-bold clickable-card__title">
<slot name="title" />
</h3>
{/if}
{#if SLOTS.body}
<p class="text-base-body-12 clickable-card__text">
<slot name="body" />
</p>
{/if}
</div>
{#if SLOTS.actions}
<div class="clickable-card__actions">
<slot name="actions" />
</div>
{/if}
</button>
<style lang="post-css">
.clickable-card {
display: flex;
gap: var(--space-16);
border-left: 1px solid var(--clr-theme-container-outline-light);
border-right: 1px solid var(--clr-theme-container-outline-light);
background-color: var(--clr-theme-container-light);
transition:
background-color var(--transition-fast),
border-color var(--transition-fast);
text-align: left;
&:hover {
background-color: color-mix(
in srgb,
var(--clr-theme-container-light),
var(--darken-tint-extralight)
);
}
}
.card-disabled {
opacity: 0.6;
pointer-events: none;
}
.clickable-card__content {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-8);
}
.clickable-card__title {
color: var(--clr-theme-scale-ntrl-0);
}
.clickable-card__text {
color: var(--clr-theme-scale-ntrl-30);
}
.clickable-card__actions {
display: flex;
flex-shrink: 0;
}
/* MODIFIERS */
.has-top-radius {
border-top: 1px solid var(--clr-theme-container-outline-light);
border-top-left-radius: var(--radius-l);
border-top-right-radius: var(--radius-l);
}
.has-bottom-radius {
border-bottom-left-radius: var(--radius-l);
border-bottom-right-radius: var(--radius-l);
}
.has-bottom-line {
border-bottom: 1px solid var(--clr-theme-container-outline-light);
}
</style>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import SectionCard from './SectionCard.svelte';
import { getCloudApiClient, type User } from '$lib/backend/cloud';
import ClickableCard from '$lib/components/ClickableCard.svelte';
import Link from '$lib/components/Link.svelte';
import Spacer from '$lib/components/Spacer.svelte';
import Toggle from '$lib/components/Toggle.svelte';
@ -33,7 +33,7 @@
dispatch('updated', { ...project, api: { ...cloudProject, sync: project.api.sync } });
});
const onSyncChange = async (event: CustomEvent<boolean>) => {
async function onSyncChange(sync: boolean) {
if (!user) return;
try {
const cloudProject =
@ -43,26 +43,26 @@
description: project.description,
uid: project.id
}));
dispatch('updated', { ...project, api: { ...cloudProject, sync: event.detail } });
dispatch('updated', { ...project, api: { ...cloudProject, sync } });
} catch (error) {
console.error(`Failed to update project sync status: ${error}`);
toasts.error('Failed to update project sync status');
}
};
}
const aiGenToggle = () => {
function aiGenToggle() {
$aiGenEnabled = !$aiGenEnabled;
$aiGenAutoBranchNamingEnabled = $aiGenEnabled;
};
}
const aiGenBranchNamesToggle = () => {
function aiGenBranchNamesToggle() {
$aiGenAutoBranchNamingEnabled = !$aiGenAutoBranchNamingEnabled;
};
}
</script>
{#if user}
<div class="aigen-wrap">
<ClickableCard on:click={aiGenToggle}>
<SectionCard on:click={aiGenToggle} orientation="row">
<svelte:fragment slot="title">Enable branch and commit message generation</svelte:fragment>
<svelte:fragment slot="body">
Uses OpenAI's API. If enabled, diffs will sent to OpenAI's servers when pressing the
@ -71,9 +71,9 @@
<svelte:fragment slot="actions">
<Toggle checked={$aiGenEnabled} on:change={aiGenToggle} />
</svelte:fragment>
</ClickableCard>
</SectionCard>
<ClickableCard disabled={!$aiGenEnabled} on:click={aiGenBranchNamesToggle}>
<SectionCard disabled={!$aiGenEnabled} on:click={aiGenBranchNamesToggle} orientation="row">
<svelte:fragment slot="title">Automatically generate branch names</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle
@ -82,7 +82,7 @@
on:change={aiGenBranchNamesToggle}
/>
</svelte:fragment>
</ClickableCard>
</SectionCard>
</div>
<Spacer />
@ -90,14 +90,14 @@
{#if user.role === 'admin'}
<h3 class="text-base-15 text-bold">Full data synchronization</h3>
<ClickableCard on:change={onSyncChange}>
<SectionCard on:change={(e) => onSyncChange(e.detail)} orientation="row">
<svelte:fragment slot="body">
Sync my history, repository and branch data for backup, sharing and team features.
</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle checked={project.api?.sync || false} on:change={onSyncChange} />
<Toggle checked={project.api?.sync || false} on:change={(e) => onSyncChange(e.detail)} />
</svelte:fragment>
</ClickableCard>
</SectionCard>
{#if project.api}
<div class="api-link">

View File

@ -1,19 +1,17 @@
<script lang="ts">
import ClickableCard from './ClickableCard.svelte';
import RadioButton from './RadioButton.svelte';
import SectionCard from './SectionCard.svelte';
import Spacer from './Spacer.svelte';
import TextBox from './TextBox.svelte';
import { invoke } from '$lib/backend/ipc';
import Button from '$lib/components/Button.svelte';
import Link from '$lib/components/Link.svelte';
import { copyToClipboard } from '$lib/utils/clipboard';
import { debounce } from '$lib/utils/debounce';
import { openExternalUrl } from '$lib/utils/url';
import { createEventDispatcher } from 'svelte';
import type { Key, Project } from '$lib/backend/projects';
import { createEventDispatcher, onMount } from 'svelte';
import type { Key, KeyType, Project } from '$lib/backend/projects';
export let project: Project;
export let sshKey = '';
const dispatch = createEventDispatcher<{
updated: {
@ -21,234 +19,186 @@
};
}>();
export function get_public_key() {
return invoke<string>('get_public_key');
}
let sshKey = '';
get_public_key().then((key) => {
sshKey = key;
});
let selectedOption =
project.preferred_key === 'generated'
? 'generated'
: project.preferred_key === 'default'
? 'default'
: project.preferred_key === 'gitCredentialsHelper'
? 'gitCredentialsHelper'
: 'local';
let selectedType: KeyType =
typeof project.preferred_key == 'string' ? project.preferred_key : 'local';
let privateKeyPath =
project.preferred_key === 'generated' ||
project.preferred_key === 'default' ||
project.preferred_key === 'gitCredentialsHelper'
? ''
: project.preferred_key.local.private_key_path;
typeof project.preferred_key == 'string' ? '' : project.preferred_key.local.private_key_path;
let privateKeyPassphrase =
project.preferred_key === 'generated' ||
project.preferred_key === 'default' ||
project.preferred_key === 'gitCredentialsHelper'
? ''
: project.preferred_key.local.passphrase;
typeof project.preferred_key == 'string' ? '' : project.preferred_key.local.passphrase;
function setLocalKey() {
if (privateKeyPath.length) {
dispatch('updated', {
preferred_key: {
local: {
private_key_path: privateKeyPath,
passphrase:
privateKeyPassphrase && privateKeyPassphrase.length > 0
? privateKeyPassphrase
: undefined
}
if (privateKeyPath.trim().length == 0) return;
dispatch('updated', {
preferred_key: {
local: {
private_key_path: privateKeyPath.trim(),
passphrase: privateKeyPassphrase || undefined
}
});
}
}
function setGitCredentialsHelperKey() {
dispatch('updated', {
preferred_key: 'gitCredentialsHelper'
});
}
function setDefaultKey() {
dispatch('updated', {
preferred_key: 'default'
});
}
function setGeneratedKey() {
dispatch('updated', {
preferred_key: 'generated'
}
});
}
let showPassphrase = false;
let form: HTMLFormElement;
function onFormChange(form: HTMLFormElement) {
const formData = new FormData(form);
selectedType = formData.get('credentialType') as KeyType;
if (selectedType != 'local') {
dispatch('updated', { preferred_key: selectedType });
} else {
setLocalKey();
}
}
onMount(() => {
form.credentialType.value = selectedType;
});
</script>
<section class="git-auth-wrap">
<div class="git-auth-wrap">
<h3 class="text-base-15 text-bold">Git Authentication</h3>
<p class="text-base-body-12">
Configure the authentication flow for GitButler when authenticating with your Git remote
provider.
</p>
<form>
<fieldset class="git-radio">
<ClickableCard
hasBottomRadius={false}
on:click={() => {
if (selectedOption == 'default') return;
<form class="git-radio" bind:this={form} on:change={(e) => onFormChange(e.currentTarget)}>
<SectionCard roundedBottom={false} orientation="row" labelFor="credential-default">
<svelte:fragment slot="title">Auto detect</svelte:fragment>
selectedOption = 'default';
setDefaultKey();
}}
>
<svelte:fragment slot="title">Auto detect</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton name="credentialType" id="credential-default" value="default" />
</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton bind:group={selectedOption} value="default" on:input={setDefaultKey} />
</svelte:fragment>
<svelte:fragment slot="body">
GitButler will attempt all available authentication flows automatically.
</svelte:fragment>
</SectionCard>
<svelte:fragment slot="body">
GitButler will attempt all available authentication flows automatically.
</svelte:fragment>
</ClickableCard>
<SectionCard
roundedTop={false}
roundedBottom={false}
bottomBorder={selectedType !== 'local'}
orientation="row"
labelFor="credential-local"
>
<svelte:fragment slot="title">Use existing SSH key</svelte:fragment>
<ClickableCard
hasTopRadius={false}
hasBottomRadius={false}
hasBottomLine={selectedOption !== 'local'}
on:click={() => {
selectedOption = 'local';
}}
>
<svelte:fragment slot="title">Use existing SSH key</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton name="credentialType" id="credential-local" value="local" />
</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton bind:group={selectedOption} value="local" />
</svelte:fragment>
<svelte:fragment slot="body">
Add the path to an existing SSH key that GitButler can use.
</svelte:fragment>
</SectionCard>
<svelte:fragment slot="body">
Add the path to an existing SSH key that GitButler can use.
</svelte:fragment>
</ClickableCard>
{#if selectedOption === 'local'}
<SectionCard hasTopRadius={false} hasBottomRadius={false}>
<div class="inputs-group">
<TextBox
label="Path to private key"
placeholder="for example: ~/.ssh/id_rsa"
bind:value={privateKeyPath}
on:input={debounce(setLocalKey, 600)}
/>
<div class="input-with-button">
<TextBox
label="Passphrase (optional)"
type={showPassphrase ? 'text' : 'password'}
bind:value={privateKeyPassphrase}
on:input={debounce(setLocalKey, 600)}
wide
/>
<Button
size="large"
color="neutral"
kind="outlined"
icon={showPassphrase ? 'eye-shown' : 'eye-hidden'}
on:click={() => (showPassphrase = !showPassphrase)}
width={150}
>
{showPassphrase ? 'Hide passphrase' : 'Show passphrase'}
</Button>
</div>
</div>
</SectionCard>
{/if}
<ClickableCard
hasTopRadius={false}
hasBottomRadius={false}
hasBottomLine={selectedOption !== 'generated'}
on:click={() => {
if (selectedOption == 'generated') return;
selectedOption = 'generated';
setGeneratedKey();
}}
>
<svelte:fragment slot="title">Use locally generated SSH key</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton bind:group={selectedOption} value="generated" on:input={setGeneratedKey} />
</svelte:fragment>
<svelte:fragment slot="body">
GitButler will use a locally generated SSH key. For this to work you need to add the
following public key to your Git remote provider:
</svelte:fragment>
</ClickableCard>
{#if selectedOption === 'generated'}
<SectionCard hasTopRadius={false} hasBottomRadius={false}>
<TextBox readonly selectall bind:value={sshKey} />
<div class="row-buttons">
<Button
kind="filled"
color="primary"
icon="copy"
on:mousedown={() => copyToClipboard(sshKey)}
>
Copy to Clipboard
</Button>
<Button
kind="outlined"
color="neutral"
icon="open-link"
on:mousedown={() => {
openExternalUrl('https://github.com/settings/ssh/new');
}}
>
Add key to GitHub
</Button>
</div>
</SectionCard>
{/if}
<ClickableCard
hasTopRadius={false}
on:click={() => {
if (selectedOption == 'gitCredentialsHelper') return;
selectedOption = 'gitCredentialsHelper';
setGitCredentialsHelperKey();
}}
>
<svelte:fragment slot="title">Use a Git credentials helper</svelte:fragment>
<svelte:fragment slot="body">
GitButler will use the system's git credentials helper.
<Link target="_blank" rel="noreferrer" href="https://git-scm.com/doc/credential-helpers">
Learn more
</Link>
</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton
bind:group={selectedOption}
value="gitCredentialsHelper"
on:input={setGitCredentialsHelperKey}
{#if selectedType == 'local'}
<SectionCard hasTopRadius={false} hasBottomRadius={false} orientation="row">
<div class="inputs-group">
<TextBox
label="Path to private key"
placeholder="for example: ~/.ssh/id_rsa"
bind:value={privateKeyPath}
/>
</svelte:fragment>
</ClickableCard>
</fieldset>
<div class="input-with-button">
<TextBox
label="Passphrase (optional)"
type={showPassphrase ? 'text' : 'password'}
bind:value={privateKeyPassphrase}
wide
/>
<Button
size="large"
color="neutral"
kind="outlined"
icon={showPassphrase ? 'eye-shown' : 'eye-hidden'}
on:click={() => (showPassphrase = !showPassphrase)}
width={150}
>
{showPassphrase ? 'Hide passphrase' : 'Show passphrase'}
</Button>
</div>
</div>
</SectionCard>
{/if}
<SectionCard
roundedTop={false}
roundedBottom={false}
bottomBorder={selectedType !== 'generated'}
orientation="row"
labelFor="credential-generated"
>
<svelte:fragment slot="title">Use locally generated SSH key</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton name="credentialType" id="credential-generated" value="generated" />
</svelte:fragment>
<svelte:fragment slot="body">
GitButler will use a locally generated SSH key. For this to work you need to add the
following public key to your Git remote provider:
</svelte:fragment>
</SectionCard>
{#if selectedType === 'generated'}
<SectionCard hasTopRadius={false} hasBottomRadius={false} orientation="row">
<TextBox readonly selectall bind:value={sshKey} />
<div class="row-buttons">
<Button
kind="filled"
color="primary"
icon="copy"
on:mousedown={() => copyToClipboard(sshKey)}
>
Copy to Clipboard
</Button>
<Button
kind="outlined"
color="neutral"
icon="open-link"
on:mousedown={() => {
openExternalUrl('https://github.com/settings/ssh/new');
}}
>
Add key to GitHub
</Button>
</div>
</SectionCard>
{/if}
<SectionCard
roundedTop={false}
roundedBottom={false}
orientation="row"
labelFor="credential-helper"
>
<svelte:fragment slot="title">Use a Git credentials helper</svelte:fragment>
<svelte:fragment slot="body">
GitButler will use the system's git credentials helper.
<Link target="_blank" rel="noreferrer" href="https://git-scm.com/doc/credential-helpers">
Learn more
</Link>
</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton name="credentialType" value="gitCredentialsHelper" id="credential-helper" />
</svelte:fragment>
</SectionCard>
<SectionCard roundedTop={false} orientation="row">
<svelte:fragment slot="body">
<Button wide>Test credentials</Button>
</svelte:fragment>
</SectionCard>
</form>
</section>
</div>
<Spacer />

View File

@ -1,6 +1,6 @@
<script lang="ts">
import SectionCard from './SectionCard.svelte';
import Spacer from './Spacer.svelte';
import ClickableCard from '$lib/components/ClickableCard.svelte';
import Toggle from '$lib/components/Toggle.svelte';
import { projectRunCommitHooks } from '$lib/config/config';
import { createEventDispatcher } from 'svelte';
@ -18,62 +18,48 @@
omit_certificate_check?: boolean;
};
}>();
const onAllowForcePushingChange = () => {
dispatch('updated', { ok_with_force_push: allowForcePushing });
};
const onOmitCertificateCheckChange = () => {
dispatch('updated', { omit_certificate_check: omitCertificateCheck });
};
const onRunCommitHooksChange = () => {
$runCommitHooks = !$runCommitHooks;
};
</script>
<section class="wrapper">
<ClickableCard
on:click={() => {
allowForcePushing = !allowForcePushing;
onAllowForcePushingChange();
}}
>
<SectionCard orientation="row" labelFor="allowForcePush">
<svelte:fragment slot="title">Allow force pushing</svelte:fragment>
<svelte:fragment slot="body">
Force pushing allows GitButler to override branches even if they were pushed to remote. We
will never force push to the trunk.
</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle bind:checked={allowForcePushing} on:change={onAllowForcePushingChange} />
<Toggle
id="allowForcePush"
bind:checked={allowForcePushing}
on:change={() => dispatch('updated', { ok_with_force_push: allowForcePushing })}
/>
</svelte:fragment>
</ClickableCard>
</SectionCard>
<ClickableCard
on:click={() => {
omitCertificateCheck = !omitCertificateCheck;
onOmitCertificateCheckChange();
}}
>
<SectionCard orientation="row" labelFor="omitCertificateCheck">
<svelte:fragment slot="title">Ignore host certificate checks</svelte:fragment>
<svelte:fragment slot="body">
Enabling this will ignore host certificate checks when authenticating with ssh.
</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle bind:checked={omitCertificateCheck} on:change={onOmitCertificateCheckChange} />
<Toggle
id="omitCertificateCheck"
bind:checked={omitCertificateCheck}
on:change={() => dispatch('updated', { omit_certificate_check: omitCertificateCheck })}
/>
</svelte:fragment>
</ClickableCard>
</SectionCard>
<ClickableCard on:click={onRunCommitHooksChange}>
<SectionCard labelFor="runHooks" orientation="row">
<svelte:fragment slot="title">Run commit hooks</svelte:fragment>
<svelte:fragment slot="body">
Enabling this will run any git pre and post commit hooks you have configured in your
repository.
</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle bind:checked={$runCommitHooks} on:change={onRunCommitHooksChange} />
<Toggle id="runHooks" bind:checked={$runCommitHooks} />
</svelte:fragment>
</ClickableCard>
</SectionCard>
</section>
<Spacer />

View File

@ -3,8 +3,8 @@
export let small = false;
export let disabled = false;
export let group = '';
export let value = '';
export let id = '';
</script>
<input
@ -14,10 +14,10 @@
type="radio"
class="radio"
class:small
{id}
{value}
{name}
{disabled}
bind:group
/>
<style lang="postcss">

View File

@ -1,18 +1,32 @@
<script lang="ts" context="module">
export type SectionCardBackground = 'loading' | 'success' | 'error' | undefined;
</script>
<script lang="ts">
export let orientation: 'row' | 'column' = 'column';
export let hasTopRadius = true;
export let hasBottomRadius = true;
export let hasBottomLine = true;
export let extraPadding = false;
export let roundedTop = true;
export let roundedBottom = true;
export let bottomBorder = true;
export let background: SectionCardBackground = undefined;
export let noBorder = false;
export let labelFor = '';
const SLOTS = $$props.$$slots;
</script>
<section
<label
for={labelFor}
class="section-card"
style:flex-direction={orientation}
class:has-top-radius={hasTopRadius}
class:has-bottom-radius={hasBottomRadius}
class:has-bottom-line={hasBottomLine}
class:extra-padding={extraPadding}
class:rounded-top={roundedTop}
class:rounded-bottom={roundedBottom}
class:bottom-border={bottomBorder}
class:no-border={noBorder}
class:loading={background == 'loading'}
class:success={background == 'success'}
class:error={background == 'error'}
>
{#if SLOTS.iconSide}
<div class="section-card__icon-side">
@ -35,7 +49,12 @@
</div>
{/if}
<slot />
</section>
{#if SLOTS.actions}
<div class="clickable-card__actions">
<slot name="actions" />
</div>
{/if}
</label>
<style lang="post-css">
.section-card {
@ -45,17 +64,34 @@
border-left: 1px solid var(--clr-theme-container-outline-light);
border-right: 1px solid var(--clr-theme-container-outline-light);
background-color: var(--clr-theme-container-light);
cursor: pointer;
transition:
background-color var(--transition-fast),
border-color var(--transition-fast);
text-align: left;
}
.loading {
background: var(--clr-theme-container-pale);
}
.success {
background: var(--clr-theme-pop-container);
}
.error {
background: var(--clr-theme-warn-container);
}
.extra-padding {
padding: var(--space-20);
}
.section-card__content {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-8);
user-select: text;
}
.section-card__title {
@ -68,20 +104,22 @@
/* MODIFIERS */
.has-top-radius {
.rounded-top {
border-top: 1px solid var(--clr-theme-container-outline-light);
border-top-left-radius: var(--radius-m);
border-top-right-radius: var(--radius-m);
}
.has-bottom-radius {
.rounded-bottom {
border-bottom-left-radius: var(--radius-m);
border-bottom-right-radius: var(--radius-m);
}
.has-bottom-line {
.bottom-border {
border-bottom: 1px solid var(--clr-theme-container-outline-light);
user-select: text;
cursor: text;
}
.no-border {
border: none;
}
</style>

View File

@ -9,6 +9,7 @@
export let checked = false;
export let value = '';
export let help = '';
export let id = '';
let input: HTMLInputElement;
const dispatch = createEventDispatcher<{ change: boolean }>();
@ -26,7 +27,7 @@
class:small
{value}
{name}
id={name}
{id}
{disabled}
use:tooltip={help}
/>

View File

@ -1,5 +1,6 @@
import { initPostHog } from '$lib/analytics/posthog';
import { initSentry } from '$lib/analytics/sentry';
import { AuthService } from '$lib/backend/auth';
import { getCloudApiClient } from '$lib/backend/cloud';
import { ProjectService } from '$lib/backend/projects';
import { UpdaterService } from '$lib/backend/updater';
@ -33,7 +34,6 @@ export const load: LayoutLoad = async ({ fetch: realFetch }: { fetch: typeof fet
if (enabled) initPostHog();
});
const userService = new UserService();
const updaterService = new UpdaterService();
// TODO: Find a workaround to avoid this dynamic import
// https://github.com/sveltejs/kit/issues/905
@ -41,9 +41,10 @@ export const load: LayoutLoad = async ({ fetch: realFetch }: { fetch: typeof fet
const defaultPath = await homeDir();
return {
authService: new AuthService(),
projectService: new ProjectService(defaultPath),
cloud: getCloudApiClient({ fetch: realFetch }),
updaterService,
updaterService: new UpdaterService(),
userService,
user$: userService.user$
};

View File

@ -11,7 +11,7 @@ import type { LayoutLoad } from './$types';
export const prerender = false;
export const load: LayoutLoad = async ({ params, parent }) => {
const { user$, projectService, userService } = await parent();
const { authService, projectService, userService } = await parent();
const projectId = params.projectId;
const project$ = projectService.getProject(projectId);
const fetches$ = getFetchNotifications(projectId);
@ -41,8 +41,11 @@ export const load: LayoutLoad = async ({ params, parent }) => {
branchController
);
const user$ = userService.user$;
return {
projectId,
authService,
branchController,
baseBranchService,
githubService,

View File

@ -8,7 +8,6 @@
import SectionCard from '$lib/components/SectionCard.svelte';
import ContentWrapper from '$lib/components/settings/ContentWrapper.svelte';
import * as toasts from '$lib/utils/toasts';
import type { UserError } from '$lib/backend/ipc';
import type { Key, Project } from '$lib/backend/projects';
import type { PageData } from './$types';
import { goto } from '$app/navigation';
@ -24,34 +23,41 @@
let deleteConfirmationModal: RemoveProjectButton;
let isDeleting = false;
const onDeleteClicked = () =>
Promise.resolve()
.then(() => (isDeleting = true))
.then(() => projectService.deleteProject($project$?.id))
.catch((e) => {
console.error(e);
toasts.error('Failed to delete project');
})
.then(() => toasts.success('Project deleted'))
.then(() => goto('/'))
.finally(() => {
isDeleting = false;
projectService.reload();
});
async function onDeleteClicked() {
isDeleting = true;
try {
projectService.deleteProject($project$?.id);
toasts.success('Project deleted');
goto('/');
} catch (err: any) {
console.error(err);
toasts.error('Failed to delete project');
} finally {
isDeleting = false;
projectService.reload();
}
}
const onKeysUpdated = (e: { detail: { preferred_key: Key } }) =>
projectService
.updateProject({ ...$project$, ...e.detail })
.then(() => toasts.success('Preferred key updated'))
.catch((e: UserError) => {
toasts.error(e.message);
});
const onCloudUpdated = (e: { detail: Project }) =>
async function onKeysUpdated(e: { detail: { preferred_key: Key } }) {
try {
projectService.updateProject({ ...$project$, ...e.detail });
toasts.success('Preferred key updated');
} catch (err: any) {
toasts.error(err.message);
}
}
async function onCloudUpdated(e: { detail: Project }) {
projectService.updateProject({ ...$project$, ...e.detail });
const onPreferencesUpdated = (e: {
}
async function onPreferencesUpdated(e: {
detail: { ok_with_force_push?: boolean; omit_certificate_check?: boolean };
}) => projectService.updateProject({ ...$project$, ...e.detail });
const onDetailsUpdated = async (e: { detail: Project }) => {
}) {
await projectService.updateProject({ ...$project$, ...e.detail });
}
async function onDetailsUpdated(e: { detail: Project }) {
const api =
$user$ && e.detail.api
? await cloud.projects.update($user$?.access_token, e.detail.api.repository_id, {
@ -59,12 +65,11 @@
description: e.detail.description
})
: undefined;
projectService.updateProject({
...e.detail,
api: api ? { ...api, sync: e.detail.api?.sync || false } : undefined
});
};
}
</script>
{#if !$project$}

View File

@ -2,7 +2,6 @@
import { deleteAllData } from '$lib/backend/data';
import AnalyticsSettings from '$lib/components/AnalyticsSettings.svelte';
import Button from '$lib/components/Button.svelte';
import ClickableCard from '$lib/components/ClickableCard.svelte';
import GithubIntegration from '$lib/components/GithubIntegration.svelte';
import Link from '$lib/components/Link.svelte';
import Login from '$lib/components/Login.svelte';
@ -19,37 +18,33 @@
import * as toasts from '$lib/utils/toasts';
import { openExternalUrl } from '$lib/utils/url';
import { invoke } from '@tauri-apps/api/tauri';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import { goto } from '$app/navigation';
export let data: PageData;
const { cloud, user$, userService } = data;
$: saving = false;
$: userPicture = $user$?.picture;
const { cloud, user$, userService, authService } = data;
const fileTypes = ['image/jpeg', 'image/png'];
const validFileType = (file: File) => {
return fileTypes.includes(file.type);
};
const onPictureChange = (e: Event) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (file && validFileType(file)) {
userPicture = URL.createObjectURL(file);
} else {
userPicture = $user$?.picture;
toasts.error('Please use a valid image file');
}
};
// TODO: Maybe break these into components?
let currentSection: 'profile' | 'git-stuff' | 'telemetry' | 'integrations' = 'profile';
// TODO: Refactor such that this variable isn't needed
let newName = '';
let loaded = false;
let isDeleting = false;
let signCommits = false;
let annotateCommits = true;
let sshKey = '';
let deleteConfirmationModal: Modal;
$: saving = false;
$: userPicture = $user$?.picture;
$: if ($user$ && !loaded) {
loaded = true;
cloud.user.get($user$?.access_token).then((cloudUser) => {
@ -59,7 +54,19 @@
newName = $user$?.name || '';
}
const onSubmit = async (e: SubmitEvent) => {
function onPictureChange(e: Event) {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (file && fileTypes.includes(file.type)) {
userPicture = URL.createObjectURL(file);
} else {
userPicture = $user$?.picture;
toasts.error('Please use a valid image file');
}
}
async function onSubmit(e: SubmitEvent) {
if (!$user$) return;
saving = true;
@ -80,74 +87,55 @@
toasts.error('Failed to update user');
}
saving = false;
};
}
let isDeleting = false;
let deleteConfirmationModal: Modal;
export function git_get_config(params: { key: string }) {
// TODO: These kinds of functions should be implemented on an injected service
function gitGetConfig(params: { key: string }) {
return invoke<string>('git_get_global_config', params);
}
export function git_set_config(params: { key: string; value: string }) {
function gitSetConfig(params: { key: string; value: string }) {
return invoke<string>('git_set_global_config', params);
}
const setCommitterSetting = (value: boolean) => {
annotateCommits = value;
git_set_config({
function toggleCommitterSigning() {
annotateCommits = !annotateCommits;
gitSetConfig({
key: 'gitbutler.gitbutlerCommitter',
value: value ? '1' : '0'
value: annotateCommits ? '1' : '0'
});
};
const setSigningSetting = (value: boolean) => {
signCommits = value;
git_set_config({
key: 'gitbutler.signCommits',
value: value ? 'true' : 'false'
});
};
export function get_public_key() {
return invoke<string>('get_public_key');
}
let sshKey = '';
get_public_key().then((key) => {
sshKey = key;
});
function toggleSigningSetting() {
signCommits = !signCommits;
gitSetConfig({
key: 'gitbutler.signCommits',
value: signCommits ? 'true' : 'false'
});
}
$: annotateCommits = true;
$: signCommits = false;
git_get_config({ key: 'gitbutler.gitbutlerCommitter' }).then((value) => {
annotateCommits = value ? value === '1' : false;
});
git_get_config({ key: 'gitbutler.signCommits' }).then((value) => {
signCommits = value ? value === 'true' : false;
});
const onDeleteClicked = () =>
Promise.resolve()
.then(() => (isDeleting = true))
.then(() => deleteAllData())
async function onDeleteClicked() {
isDeleting = true;
try {
deleteAllData();
await userService.logout();
// TODO: Delete user from observable!!!
.then(() => userService.logout())
.then(() => toasts.success('All data deleted'))
.catch((e) => {
console.error(e);
toasts.error('Failed to delete project');
})
.then(() => deleteConfirmationModal.close())
.then(() => goto('/', { replaceState: true, invalidateAll: true }))
.finally(() => (isDeleting = false));
toasts.success('All data deleted');
goto('/', { replaceState: true, invalidateAll: true });
} catch (err: any) {
console.error(err);
toasts.error('Failed to delete project');
} finally {
deleteConfirmationModal.close();
isDeleting = false;
}
}
let currentSection: 'profile' | 'git-stuff' | 'telemetry' | 'integrations' = 'profile';
const toggleGBCommiter = () => setCommitterSetting(!annotateCommits);
const toggleGBSigner = () => setSigningSetting(!signCommits);
onMount(async () => {
sshKey = await authService.getPublicKey();
annotateCommits = (await gitGetConfig({ key: 'gitbutler.gitbutlerCommitter' })) == '1';
signCommits = (await gitGetConfig({ key: 'gitbutler.signCommits' })) == 'true';
});
</script>
<section class="profile-page">
@ -233,7 +221,7 @@
</ContentWrapper>
{:else if currentSection === 'git-stuff'}
<ContentWrapper title="Git stuff">
<ClickableCard on:click={toggleGBCommiter}>
<SectionCard labelFor="committerSigning" orientation="row">
<svelte:fragment slot="title">Credit GitButler as the Committer</svelte:fragment>
<svelte:fragment slot="body">
By default, everything in the GitButler client is free to use. You can opt in to crediting
@ -247,9 +235,13 @@
</Link>
</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle checked={annotateCommits} on:change={toggleGBCommiter} />
<Toggle
id="commiterSigning"
checked={annotateCommits}
on:change={toggleCommitterSigning}
/>
</svelte:fragment>
</ClickableCard>
</SectionCard>
<Spacer />
@ -283,7 +275,7 @@
</div>
</SectionCard>
<ClickableCard on:click={toggleGBSigner}>
<SectionCard labelFor="signingSetting" orientation="row">
<svelte:fragment slot="title">Sign Commits with the above SSH Key</svelte:fragment>
<svelte:fragment slot="body">
If you want GitButler to sign your commits with the SSH key we generated, then you can add
@ -297,9 +289,9 @@
</Link>
</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle checked={signCommits} on:change={toggleGBSigner} />
<Toggle id="signingSetting" checked={signCommits} on:change={toggleSigningSetting} />
</svelte:fragment>
</ClickableCard>
</SectionCard>
</ContentWrapper>
{:else if currentSection === 'telemetry'}
<ContentWrapper title="Telemetry">

View File

@ -0,0 +1,14 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 14.5V7.5C15 4.73858 12.7614 2.5 10 2.5V2.5C7.23858 2.5 5 4.73858 5 7.5V14.5" stroke="#67C2BE" stroke-width="2"/>
<rect x="1" y="8.5" width="18" height="11" rx="2" fill="url(#paint0_linear_1756_54140)"/>
<rect x="1" y="8.5" width="18" height="11" rx="2" fill="#67C2BE"/>
<path d="M7 14C7 14.8284 6.32843 15.5 5.5 15.5C4.67157 15.5 4 14.8284 4 14C4 13.1716 4.67157 12.5 5.5 12.5C6.32843 12.5 7 13.1716 7 14Z" fill="black"/>
<path d="M11.5 14C11.5 14.8284 10.8284 15.5 10 15.5C9.17157 15.5 8.5 14.8284 8.5 14C8.5 13.1716 9.17157 12.5 10 12.5C10.8284 12.5 11.5 13.1716 11.5 14Z" fill="black"/>
<path d="M16 14C16 14.8284 15.3284 15.5 14.5 15.5C13.6716 15.5 13 14.8284 13 14C13 13.1716 13.6716 12.5 14.5 12.5C15.3284 12.5 16 13.1716 16 14Z" fill="black"/>
<defs>
<linearGradient id="paint0_linear_1756_54140" x1="10" y1="8.5" x2="-0.60513" y2="33.6738" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFBA52"/>
<stop offset="1" stop-color="#B8873E"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB