Run svelte-check in CI
Diff
chartered-frontend/package.json | 2 +-
chartered-frontend/src/global.d.ts | 5 -----
chartered-frontend/src/util.ts | 5 +++++
chartered-frontend/src/components/Icon.svelte | 6 +++---
chartered-frontend/src/components/Nav.svelte | 22 +++++++++++++++++++---
chartered-frontend/src/stores/auth.ts | 14 +++++++++++---
chartered-frontend/src/types/crate.ts | 25 +++++++++++++++++++++++++
chartered-frontend/src/types/organisations.ts | 7 -------
chartered-frontend/src/types/ssh_keys.ts | 2 +-
chartered-frontend/src/types/user.ts | 10 ++++++++++
chartered-frontend/src/routes/(unauthed)/+layout.svelte | 2 +-
chartered-frontend/src/routes/(authed)/search/+page.svelte | 3 ++-
chartered-frontend/src/routes/(authed)/ssh-keys/CreateKeyForm.svelte | 7 ++++---
chartered-frontend/src/routes/(authed)/ssh-keys/DeleteSshKeyModal.svelte | 9 +++++----
chartered-frontend/src/routes/(authed)/crates/[organisation]/+page.svelte | 13 +++++++------
chartered-frontend/src/routes/(authed)/crates/[organisation]/AddMember.svelte | 20 ++++++++------------
chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte | 10 ++++++----
chartered-frontend/src/routes/(authed)/organisations/create/+page.svelte | 5 +++--
chartered-frontend/src/routes/(authed)/organisations/list/+page.svelte | 2 +-
chartered-frontend/src/routes/(authed)/sessions/list/+page.svelte | 2 +-
chartered-frontend/src/routes/(authed)/sessions/list/DeleteSessionModal.svelte | 9 +++++----
chartered-frontend/src/routes/(authed)/users/[uuid]/+page.svelte | 4 ++--
chartered-frontend/src/routes/(unauthed)/auth/login/+page.svelte | 7 ++++---
chartered-frontend/src/routes/(unauthed)/auth/register/+page.svelte | 5 +++--
chartered-frontend/src/routes/(unauthed)/login/oauth/+page.svelte | 2 +-
chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/+page.svelte | 8 ++++----
chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/Dependency.svelte | 8 ++++----
chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/MemberTab.svelte | 7 ++++---
28 files changed, 139 insertions(+), 82 deletions(-)
@@ -9,7 +9,7 @@
"test": "playwright test",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"lint": "prettier --check . && eslint . && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"format": "prettier --write ."
},
"devDependencies": {
@@ -1,5 +1,0 @@
declare global {
interface Window {
extendSessionInterval: nodejs.Timer;
}
}
@@ -1,0 +1,5 @@
export function getErrorMessage(error: unknown) {
if (error instanceof Error) return error.message;
return String(error);
}
@@ -1,11 +1,11 @@
<script type="typescript">
import feather from 'feather-icons';
export const directions = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
export let name;
export let name: string;
export let direction = 'n';
export let strokeWidth = null;
export let stroke = null;
export let strokeWidth: string | null = null;
export let stroke: string | null = null;
export let width = '1em';
export let height = '1em';
@@ -25,6 +25,20 @@
async function performSearch() {
await goto(`/search?q=${search}`);
}
/**
* Prevents the dropdown from automatically closing if it was a non-link element
* that was clicked.
*
* @param e mouse click event
*/
function handleDropdownClick(e: MouseEvent) {
let tagName = (e.target as HTMLElement)?.tagName.toLowerCase();
if (tagName !== 'a' && tagName != 'button') {
e.stopPropagation();
}
}
</script>
<svelte:window on:click={() => (userDropdownShown = false)} />
@@ -68,8 +82,8 @@
class="ml-2 relative flex items-center text-gray-300 dark:text-inherit"
>
<span class="sr-only">Open user menu</span>
{#if $auth.picture_url}
<img alt="You" src={$auth.picture_url} class="rounded-[50%] h-[2.4rem] inline mr-0.5" />
{#if $auth?.picture_url}
<img alt="You" src={$auth?.picture_url} class="rounded-[50%] h-[2.4rem] inline mr-0.5" />
{:else}
<div
class="h-[2.4rem] w-[2.4rem] rounded-[50%] inline mr-0.5 bg-gray-100 dark:bg-gray-800 overflow-hidden"
@@ -83,12 +97,12 @@
<div
class:hidden={!userDropdownShown}
class="absolute top-7 right-0 z-50 w-[10rem] my-4 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
on:click={(e) => (e.target.tagName.toLowerCase() === 'a' ? null : e.stopPropagation())}
on:click={handleDropdownClick}
>
<ul class="py-1">
<li>
<a
href={`/users/${$auth.uuid}`}
href={`/users/${$auth?.uuid}`}
class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
>
Profile
@@ -1,5 +1,6 @@
import { get, writable } from 'svelte/store';
import { goto } from '$app/navigation';
import { getErrorMessage } from '../util';
/**
* The base URL of the chartered-web instance
@@ -82,7 +83,7 @@
if (e === 'Expired auth token') {
if (getErrorMessage(e) === 'Expired auth token') {
auth.set(null);
} else {
console.error('Failed to extend user session', e);
@@ -90,6 +91,12 @@
}
}
declare global {
interface Window {
extendSessionInterval: NodeJS.Timer;
}
}
if (!window.extendSessionInterval) {
extendSession();
@@ -245,8 +252,9 @@
/**
* Grab all the possible authentication methods from the backend.
*/
export function fetchOAuthProviders(): Promise<OAuthProviders> {
return fetch(`${BASE_URL}/a/-/web/v1/auth/login/oauth/providers`).then((v) => v.json());
export async function fetchOAuthProviders(): Promise<OAuthProviders> {
const result = await fetch(`${BASE_URL}/a/-/web/v1/auth/login/oauth/providers`);
return await result.json();
}
/**
@@ -29,3 +29,28 @@
display_name: string;
picture_url?: string;
}
export interface Search {
crates: SearchCrate[];
}
export interface SearchCrate {
organisation: string;
name: string;
description?: string;
version: string;
homepage?: string;
repository?: string;
}
export interface CrateMembers {
possible_permissions: string[];
members: CrateMember[];
}
export interface CrateMember {
uuid: string;
display_name: string;
picture_url?: string;
permissions: string[];
}
@@ -16,10 +16,3 @@
name: string;
description: string;
}
export interface OrganisationMember {
uuid: string;
display_name: string;
picture_url?: string;
permissions: string[];
}
@@ -1,8 +1,8 @@
/**
* The result of a `GET /web/v1/ssh-key`
*/
export interface SshKeys {
keys: { [k: number]: SshKey };
keys: SshKey[];
}
/**
@@ -7,3 +7,13 @@
external_profile_url?: string;
picture_url?: string;
}
export interface UserSearch {
users: UserSearchUser[];
}
export interface UserSearchUser {
user_uuid: string;
display_name: string;
picture_url: string;
}
@@ -1,5 +1,5 @@
<script type="typescript">
import { auth } from '../../stores/auth.ts';
import { auth } from '../../stores/auth';
import { goto } from '$app/navigation';
$: if ($auth) {
@@ -1,9 +1,10 @@
<script type="typescript">
import { page } from '$app/stores';
import { request } from '../../../stores/auth';
import Spinner from '../../../components/Spinner.svelte';
import type { Search } from '../../../types/crate';
let searchPromise;
let searchPromise: Promise<Search>;
$: searchPromise = request(`/web/v1/crates/search?q=${$page.url.searchParams.get('q')}`);
</script>
@@ -5,6 +5,7 @@
import { createEventDispatcher } from 'svelte';
import Spinner from '../../../components/Spinner.svelte';
import ErrorAlert from '../../../components/ErrorAlert.svelte';
import { getErrorMessage } from '../../../util';
// set up the event dispatcher for alerting the main page when the user has successfully
// added a key, so it can reload the full key list from the backend again
@@ -25,7 +26,7 @@
* Any errors that came of the last submission. If this is not null the user will be shown
* an alert.
*/
let error = null;
let error: string | null = null;
/**
* Submits an SSH key to the server to attempt to add it to the user, all our validation
@@ -40,7 +41,7 @@
try {
// submit the key to the backend
let result = await fetch(`${BASE_URL}/a/${$auth.auth_key}/web/v1/ssh-key`, {
let result = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/web/v1/ssh-key`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@@ -58,7 +59,7 @@
sshKey = '';
dispatch('complete');
} catch (e) {
error = e.toString();
error = getErrorMessage(e);
} finally {
// stop showing the spinner since we've finished working
submitting = false;
@@ -6,6 +6,7 @@
import RelativeTime from '../../../components/RelativeTime.svelte';
import Spinner from '../../../components/Spinner.svelte';
import { auth, BASE_URL } from '../../../stores/auth';
import { getErrorMessage } from '../../../util';
const dispatch = createEventDispatcher();
@@ -18,7 +19,7 @@
* Any errors that came of the last submission. If this is not null the user will be shown
* an alert.
*/
let deleteError = null;
let deleteError: string | null = null;
/**
* Simple boolean flag to determine if a key is currently being submitted, so we can
@@ -29,7 +30,7 @@
/**
* Binds helper keys for the modal, so we can provide some expected functions of a modal.
*/
function handleKeydown(event) {
function handleKeydown(event: KeyboardEvent) {
if (event.key == 'Escape') {
closeModal();
}
@@ -48,7 +49,7 @@
try {
// submit deletion request to backend
let res = await fetch(`${BASE_URL}/a/${$auth.auth_key}/web/v1/ssh-key/${deleting.uuid}`, {
let res = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/web/v1/ssh-key/${deleting.uuid}`, {
method: 'DELETE',
});
let json: DeleteSshKeyResult = await res.json();
@@ -64,7 +65,7 @@
closeModal();
} catch (e) {
isDeleting = false;
deleteError = e.toString();
deleteError = getErrorMessage(e);
}
}
@@ -7,12 +7,13 @@
import type { OrganisationDetail } from '../../../../types/organisations';
import Member from './Member.svelte';
import AddMember from './AddMember.svelte';
import type { CrateMembers, CrateMember } from '../../../../types/crate';
let organisationPromise;
$: organisationPromise = request<OrganisationDetail>(`/web/v1/organisations/${$page.params.organisation}`);
let organisationPromise: Promise<OrganisationDetail & CrateMembers>;
$: organisationPromise = request(`/web/v1/organisations/${$page.params.organisation}`);
function reload(event) {
organisationPromise = request<OrganisationDetail>(`/web/v1/organisations/${$page.params.organisation}`);
function reload(event: { detail: string }) {
organisationPromise = request(`/web/v1/organisations/${$page.params.organisation}`);
if (newMember && event.detail === newMember.uuid) {
newMember = null;
@@ -38,7 +39,7 @@
];
let currentTab = Tab.CRATES;
let newMember = null;
let newMember: CrateMember | null = null;
</script>
<header>
@@ -138,7 +139,7 @@
{/if}
{:else if currentTab === Tab.MEMBERS}
<div class="card p-0 divide-y card-divide">
{#each organisation.members as member, i}
{#each organisation.members as member}
<Member
{member}
organisation={$page.params.organisation}
@@ -1,9 +1,10 @@
<script type="typescript">
import { debounce } from 'lodash';
import { auth, BASE_URL } from '../../../../stores/auth';
import { request } from '../../../../stores/auth';
import Spinner from '../../../../components/Spinner.svelte';
import Icon from '../../../../components/Icon.svelte';
import { createEventDispatcher } from 'svelte';
import type { UserSearch, UserSearchUser } from '../../../../types/user';
const dispatch = createEventDispatcher();
@@ -12,7 +13,7 @@
let loading = false;
let search = '';
let searchResults = [];
let searchResults: UserSearchUser[] = [];
$: performSearch(search);
const onInput = debounce((event) => {
@@ -27,22 +28,17 @@
loading = true;
try {
let result = await fetch(`${BASE_URL}/a/${$auth.auth_key}/web/v1/users/search?q=${search}`);
let json = await result.json();
if (json.error) {
throw new Error(json.error);
}
searchResults = json.users || [];
} catch (e) {
let result = await request<UserSearch>(`/web/v1/users/search?q=${search}`);
searchResults = result.users || [];
} catch (e: unknown) {
console.log(e);
} finally {
loading = false;
}
}
function dispatchNewMember(member) {
function dispatchNewMember(member: UserSearchUser) {
dispatch('new', member);
searchResults = [];
search = '';
@@ -5,6 +5,8 @@
import { auth, BASE_URL } from '../../../../stores/auth';
import ErrorAlert from '../../../../components/ErrorAlert.svelte';
import { createEventDispatcher } from 'svelte';
import type { CrateMember } from '../../../../types/crate';
import { getErrorMessage } from '../../../../util';
const dispatch = createEventDispatcher();
@@ -12,13 +14,13 @@
export let organisation: string;
export let crate: string | null = null;
export let member;
export let member: CrateMember;
export let newPermissions = member.permissions;
export let possiblePermissions: string[];
export { clazz as class };
let saving = false;
let error = null;
let error: string | null = null;
async function save() {
saving = true;
@@ -41,7 +43,7 @@
url = `web/v1/organisations/${organisation}`;
}
let result = await fetch(`${BASE_URL}/a/${$auth.auth_key}/${url}/members`, {
let result = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/${url}/members`, {
method,
headers: {
Accept: 'application/json',
@@ -62,7 +64,7 @@
member.permissions = newPermissions;
dispatch('updated', member.uuid);
} catch (e) {
error = e.toString();
error = getErrorMessage(e);
} finally {
saving = false;
}
@@ -1,9 +1,10 @@
<script type="typescript">
import Icon from '../../../../components/Icon.svelte';
import ErrorAlert from '../../../../components/ErrorAlert.svelte';
import { BASE_URL, auth } from '../../../../stores/auth';
import { goto } from '$app/navigation';
import Spinner from '../../../../components/Spinner.svelte';
import { getErrorMessage } from '../../../../util';
/**
* Binding to the name field in the form.
@@ -43,7 +44,7 @@
try {
// attempt the actual creation of the organisation
let result = await fetch(`${BASE_URL}/a/${$auth.auth_key}/web/v1/organisations`, {
let result = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/web/v1/organisations`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@@ -59,7 +60,7 @@
// successful creation, return the user back to the list
await goto('/organisations/list');
} catch (e) {
error = e.toString();
error = getErrorMessage(e);
} finally {
// stop showing the spinner once creation is complete
submitting = false;
@@ -33,7 +33,7 @@
class:hidden={organisations.organisations.length === 0}
class="mb-4 grid md:grid-cols-2 lg:grid-cols-4 gap-2"
>
{#each organisations.organisations as organisation, i}
{#each organisations.organisations as organisation}
<a class="card flex space-x-2 items-center" href={`/crates/${organisation.name}`}>
<div class="flex-grow h-full">
<h5 class="text-highlight card-header">{organisation.name}</h5>
@@ -77,7 +77,7 @@
<DeleteSessionModal {deleting} on:complete={reloadSessions} on:close={() => (deleting = null)} />
{/if}
<style>
<style lang="postcss">
th,
td {
@apply text-left px-5;
@@ -5,6 +5,7 @@
import Spinner from '../../../../components/Spinner.svelte';
import { auth, BASE_URL } from '../../../../stores/auth';
import type { Session } from '../../../../types/sessions';
import { getErrorMessage } from '../../../../util';
const dispatch = createEventDispatcher();
@@ -17,7 +18,7 @@
* Any errors that came of the last submission. If this is not null the user will be shown
* an alert.
*/
let deleteError = null;
let deleteError: string | null = null;
/**
* Simple boolean flag to determine if a session is currently being submitted, so we can
@@ -28,7 +29,7 @@
/**
* Binds helper keys for the modal, so we can provide some expected functions of a modal.
*/
function handleKeydown(event) {
function handleKeydown(event: KeyboardEvent) {
if (event.key == 'Escape') {
closeModal();
}
@@ -47,7 +48,7 @@
try {
// submit deletion request to backend
let res = await fetch(`${BASE_URL}/a/${$auth.auth_key}/web/v1/sessions`, {
let res = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/web/v1/sessions`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
@@ -67,7 +68,7 @@
closeModal();
} catch (e) {
isDeleting = false;
deleteError = e.toString();
deleteError = getErrorMessage(e);
}
}
@@ -5,9 +5,9 @@
import Icon from '../../../../components/Icon.svelte';
import type { User } from '../../../../types/user';
let userPromise;
let userPromise: Promise<User & { displayName?: string }>;
$: userPromise = request<User>(`/web/v1/users/info/${$page.params.uuid}`).then(
(user: User & { displayName: string }) => {
(user: User & { displayName?: string }) => {
user.displayName = user.nick || user.name || user.username;
return user;
},
@@ -1,8 +1,9 @@
<script type="typescript">
import { loginOAuth, login, fetchOAuthProviders } from '../../../../stores/auth';
import Spinner from '../../../../components/Spinner.svelte';
import ErrorAlert from '../../../../components/ErrorAlert.svelte';
import { goto } from '$app/navigation';
import { getErrorMessage } from '../../../../util';
/**
* Once OAuth providers are loaded this will contain whether password auth
@@ -20,7 +21,7 @@
/**
* Displays an error to the user, if empty will hide the error popup.
*/
let error = null;
let error: string | null = null;
/**
* A binding to the username field in the form.
@@ -42,7 +43,7 @@
try {
await login(username, password);
} catch (e) {
error = e.toString();
error = getErrorMessage(e);
} finally {
// behave like a real application and set the password to empty so
// if the user fails authentication they can type it in again
@@ -72,7 +73,7 @@
// attempt to redirect the user to the OAuth login page
await loginOAuth(provider);
} catch (e) {
error = e.toString();
error = getErrorMessage(e);
} finally {
loginInProgress = false;
}
@@ -1,8 +1,9 @@
<script type="typescript">
import Spinner from '../../../../components/Spinner.svelte';
import ErrorAlert from '../../../../components/ErrorAlert.svelte';
import { register } from '../../../../stores/auth';
import { goto } from '$app/navigation';
import { getErrorMessage } from '../../../../util';
/**
* Displays a spinner while registration is being performed.
@@ -12,7 +13,7 @@
/**
* Displays an error message to the user above the form if set to a string
*/
let error = null;
let error: string | null = null;
/**
* The username bound to the corresponding form field
@@ -44,7 +45,7 @@
// redirect the user back to the login
await goto('/auth/login');
} catch (e) {
error = e.toString();
error = getErrorMessage(e);
} finally {
// stop displaying the spinner since we've finished attempting to register the
// user
@@ -10,7 +10,7 @@
<div class="h-[18rem]">
{#await callback}
<Spinner />
{:then x}
{:then}
<Spinner />
{:catch error}
@@ -33,8 +33,8 @@
},
];
let cratePromise;
$: cratePromise = request<Crate>(`/web/v1/crates/${$page.params.organisation}/${$page.params.crate}`);
let cratePromise: Promise<Crate>;
$: cratePromise = request(`/web/v1/crates/${$page.params.organisation}/${$page.params.crate}`);
let currentTab = Tab.README;
</script>
@@ -129,7 +129,7 @@
<h1 class="text-xl p-3 border-b border-gray-200 dark:border-gray-700 font-medium">Dependencies</h1>
<div class="divide-y divide-gray-200 dark:divide-gray-700">
{#each crate.versions[0].deps as dependency, i}
{#each crate.versions[0].deps as dependency}
<Dependency {dependency} class="p-3" />
{/each}
</div>
@@ -138,7 +138,7 @@
{/await}
</main>
<style>
<style lang="postcss">
.card-header-button {
@apply inline-flex items-center gap-x-1;
}
@@ -6,10 +6,10 @@
export let dependency: VersionDependency;
export { clazz as class };
function getLocalDependencyOrganisation(): string {
const s = dependency.registry.split('/');
function getLocalDependencyOrganisation(): string | undefined {
const s = dependency.registry?.split('/');
return s[s.length - 1];
return s ? s[s.length - 1] : dependency.registry;
}
</script>
@@ -18,7 +18,7 @@
<a href={`https://crates.io/crates/${dependency.name}`} target="_blank">
{dependency.name}
</a>
{:else if dependency.registry.indexOf('ssh://') === 0}
{:else if dependency.registry?.indexOf('ssh://') === 0}
<a href={`/crates/${getLocalDependencyOrganisation()}/${dependency.name}`} target="_blank">
{dependency.name}
</a>
@@ -1,12 +1,13 @@
<script type="typescript">
import { request } from '../../../../../stores/auth';
import { page } from '$app/stores';
import AddMember from '../AddMember.svelte';
import Member from '../Member.svelte';
import type { CrateMembers, CrateMember } from '../../../../../types/crate';
let newMember = null;
let newMember: CrateMember | null = null;
let membersPromise;
let membersPromise: Promise<CrateMembers>;
$: membersPromise = request(`/web/v1/crates/${$page.params.organisation}/${$page.params.crate}/members`);
function reloadMembers() {
@@ -17,7 +18,7 @@
{#await membersPromise then members}
<div class="card-divide divide-y">
{#each members.members as member, i}
{#each members.members as member}
<Member
{member}
organisation={$page.params.organisation}