mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into 'spreadsheet'.
This commit is contained in:
@@ -9,17 +9,22 @@
|
||||
upgradeURL
|
||||
} from '$lib/stores/billing';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { onMount } from 'svelte';
|
||||
import SelectProjectCloud from './selectProjectCloud.svelte';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
let showSelectProject: boolean = $state(false);
|
||||
|
||||
let selectedProjects: string[] = $state([]);
|
||||
onMount(() => {
|
||||
selectedProjects = page.data.organization?.projects || [];
|
||||
let showSelectProject: boolean = $state(false);
|
||||
|
||||
const organizationId = $derived(page.data.organization?.$id);
|
||||
|
||||
$effect(() => {
|
||||
if (organizationId) {
|
||||
selectedProjects = page.data.organization?.projects || [];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<SelectProjectCloud bind:showSelectProject bind:selectedProjects />
|
||||
<SelectProjectCloud bind:showSelectProject bind:selectedProjects {organizationId} />
|
||||
|
||||
{#if $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import { Alert, Button, Table } from '@appwrite.io/pink-svelte';
|
||||
import { type Models, Query } from '@appwrite.io/console';
|
||||
import { Alert, Button, Skeleton, Table } from '@appwrite.io/pink-svelte';
|
||||
import { Modal } from '$lib/components';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { billingProjectsLimitDate } from '$lib/stores/billing';
|
||||
import { page } from '$app/state';
|
||||
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
|
||||
let {
|
||||
showSelectProject = $bindable(false),
|
||||
selectedProjects = $bindable([])
|
||||
selectedProjects = $bindable([]),
|
||||
organizationId
|
||||
}: {
|
||||
showSelectProject: boolean;
|
||||
selectedProjects: string[];
|
||||
organizationId: string;
|
||||
} = $props();
|
||||
|
||||
let projects = $state<Array<Models.Project>>([]);
|
||||
let error = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let projectsLoadingError = $state<string | null>(null);
|
||||
|
||||
onMount(() => {
|
||||
const currentOrgId = page.data.organization?.$id;
|
||||
projects =
|
||||
page.data.currentOrgId === currentOrgId ? page.data.allProjects?.projects || [] : [];
|
||||
});
|
||||
let error = $state<string | null>(null);
|
||||
let projects = $state<Array<Models.Project>>([]);
|
||||
|
||||
let projectsToArchive = $derived(
|
||||
projects.filter((project) => !selectedProjects.includes(project.$id))
|
||||
@@ -39,6 +36,7 @@
|
||||
projects[0].teamId,
|
||||
selectedProjects
|
||||
);
|
||||
|
||||
showSelectProject = false;
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
addNotification({
|
||||
@@ -69,47 +67,95 @@
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!showSelectProject) return;
|
||||
|
||||
const areProjectsLoaded = projects.length > 0;
|
||||
const teamIdInLoadedProjects = areProjectsLoaded ? projects[0].teamId : null;
|
||||
|
||||
if (organizationId != teamIdInLoadedProjects) {
|
||||
loading = true;
|
||||
|
||||
sdk.forConsole.projects
|
||||
.list([
|
||||
Query.equal('teamId', organizationId),
|
||||
Query.limit(1000) // Get all projects for organization
|
||||
])
|
||||
.then((loadedProjects) => (projects = loadedProjects.projects))
|
||||
.catch((err) => (projectsLoadingError = err.message))
|
||||
.finally(() => (loading = false));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal bind:show={showSelectProject} title={'Manage projects'} onSubmit={updateSelected}>
|
||||
<Modal bind:show={showSelectProject} title="Manage projects" onSubmit={updateSelected}>
|
||||
<svelte:fragment slot="description">
|
||||
Choose which {$currentPlan?.projects || 2} projects to keep. Projects over the limit will be
|
||||
blocked after this date.
|
||||
</svelte:fragment>
|
||||
{#if error}
|
||||
<Alert.Inline status="error" title="Error">{error}</Alert.Inline>
|
||||
{/if}
|
||||
<Table.Root
|
||||
let:root
|
||||
allowSelection
|
||||
bind:selectedRows={selectedProjects}
|
||||
columns={[{ id: 'name' }, { id: 'created' }]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
|
||||
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each projects as project}
|
||||
<Table.Row.Base {root} id={project.$id}>
|
||||
<Table.Cell column="name" {root}>{project.name}</Table.Cell>
|
||||
<Table.Cell column="created" {root}
|
||||
>{toLocaleDateTime(project.$createdAt)}</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{#if selectedProjects.length > $currentPlan?.projects}
|
||||
<div class="u-text-warning u-mb-4">
|
||||
You can only select {$currentPlan?.projects} projects. Please deselect others to continue.
|
||||
|
||||
{#if loading}
|
||||
<div class="skeleton-projects">
|
||||
<Table.Root let:root allowSelection columns={[{ id: 'name' }, { id: 'created' }]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
|
||||
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
|
||||
{#each Array.from({ length: 5 }) as _}
|
||||
<Table.Row.Base {root} select="disabled">
|
||||
<Table.Cell column="name" {root}>
|
||||
<Skeleton variant="line" height={20} width="80%" />
|
||||
</Table.Cell>
|
||||
<Table.Cell column="created" {root}>
|
||||
<Skeleton variant="line" height={20} width="80%" />
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedProjects.length === $currentPlan?.projects}
|
||||
<Alert.Inline
|
||||
status="warning"
|
||||
title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
|
||||
<span>
|
||||
{@html formatProjectsToArchive()}
|
||||
will be archived.
|
||||
</span>
|
||||
</Alert.Inline>
|
||||
{:else if projectsLoadingError}
|
||||
<Alert.Inline status="error" title="Error">{projectsLoadingError}</Alert.Inline>
|
||||
{:else}
|
||||
{#if error}
|
||||
<Alert.Inline status="error" title="Error">{error}</Alert.Inline>
|
||||
{/if}
|
||||
|
||||
<Table.Root
|
||||
let:root
|
||||
allowSelection
|
||||
bind:selectedRows={selectedProjects}
|
||||
columns={[{ id: 'name' }, { id: 'created' }]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
|
||||
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each projects as project}
|
||||
<Table.Row.Base {root} id={project.$id}>
|
||||
<Table.Cell column="name" {root}>{project.name}</Table.Cell>
|
||||
<Table.Cell column="created" {root}
|
||||
>{toLocaleDateTime(project.$createdAt)}</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
|
||||
{#if selectedProjects.length > $currentPlan?.projects}
|
||||
<div class="u-text-warning u-mb-4">
|
||||
You can only select {$currentPlan?.projects} projects. Please deselect others to continue.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedProjects.length === $currentPlan?.projects}
|
||||
<Alert.Inline
|
||||
status="warning"
|
||||
title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
|
||||
<span>
|
||||
{@html formatProjectsToArchive()}
|
||||
will be archived.
|
||||
</span>
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
<Button.Button size="s" variant="secondary" on:click={() => (showSelectProject = false)}
|
||||
@@ -118,3 +164,13 @@
|
||||
>Save</Button.Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
/* disable the top select all selector button */
|
||||
.skeleton-projects :global([role='rowheader'] button) {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
background: var(--bgcolor-neutral-tertiary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
import { base } from '$app/paths';
|
||||
import { currentPlan, newOrgModal } from '$lib/stores/organization';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { type Models, Query } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { page } from '$app/state';
|
||||
|
||||
type Organization = {
|
||||
name: string;
|
||||
@@ -63,15 +65,15 @@
|
||||
}
|
||||
} = createMenu();
|
||||
|
||||
let isLoadingProjects = true;
|
||||
let isLoadingProjects = false;
|
||||
let loadedProjects: Models.ProjectList = { total: 0, projects: [] };
|
||||
|
||||
export let organizations: Organization[] = [];
|
||||
export let currentProject: Models.Project | null = null;
|
||||
export let projects: Promise<Models.ProjectList> = Promise.resolve(loadedProjects);
|
||||
|
||||
let projectsBottomSheetOpen = false;
|
||||
let organisationBottomSheetOpen = false;
|
||||
let projectsBottomSheet: Promise<SheetMenu | null> = null;
|
||||
|
||||
function createOrg() {
|
||||
trackEvent(Click.OrganizationClickCreate, { source: 'breadcrumbs' });
|
||||
@@ -99,13 +101,27 @@
|
||||
}
|
||||
};
|
||||
|
||||
async function createProjectsBottomSheet(organization: Organization): Promise<SheetMenu> {
|
||||
async function createProjectsBottomSheet(
|
||||
organization: Organization
|
||||
): Promise<SheetMenu | null> {
|
||||
if (!isOnProjects) return null;
|
||||
|
||||
isLoadingProjects = true;
|
||||
|
||||
// null on non-org/project path like `onboarding`.
|
||||
loadedProjects = (await projects) ?? loadedProjects;
|
||||
loadedProjects =
|
||||
(await sdk.forConsole.projects.list({
|
||||
queries: [
|
||||
Query.equal('teamId', organizationId ?? page.data.currentOrgId),
|
||||
Query.limit(5),
|
||||
Query.orderDesc('$updatedAt')
|
||||
]
|
||||
})) ?? loadedProjects;
|
||||
|
||||
for (const project of loadedProjects.projects) {
|
||||
project.region ??= 'default';
|
||||
}
|
||||
|
||||
isLoadingProjects = false;
|
||||
|
||||
const createProjectItem = {
|
||||
@@ -185,8 +201,6 @@
|
||||
|
||||
$: selectedOrg = organizations.find((org) => org.isSelected);
|
||||
|
||||
$: projectsBottomSheet = createProjectsBottomSheet(selectedOrg);
|
||||
|
||||
$: organizationsBottomSheet = createOrganizationBottomSheet(selectedOrg);
|
||||
|
||||
$: correctPlanName =
|
||||
@@ -198,6 +212,20 @@
|
||||
: selectedOrg?.tierName; // fallback
|
||||
|
||||
$: derivedKey = `${selectedOrg?.$id}-${currentProject?.$id}`;
|
||||
|
||||
$: organizationId = currentProject?.teamId;
|
||||
|
||||
$: isOnProjects = page.route.id.includes('project-[region]-[project]');
|
||||
|
||||
$: shouldReloadProjects = isLoadingProjects
|
||||
? false
|
||||
: currentProject && loadedProjects.projects.length
|
||||
? loadedProjects.projects[0].teamId != currentProject.teamId
|
||||
: !loadedProjects.projects.length;
|
||||
|
||||
$: if (shouldReloadProjects) {
|
||||
projectsBottomSheet = createProjectsBottomSheet(selectedOrg);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={onResize} />
|
||||
@@ -380,7 +408,9 @@
|
||||
<BottomSheet.Menu bind:isOpen={organisationBottomSheetOpen} menu={organizationsBottomSheet} />
|
||||
|
||||
{#await projectsBottomSheet then menu}
|
||||
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} {menu} />
|
||||
{#if menu}
|
||||
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} {menu} />
|
||||
{/if}
|
||||
{/await}
|
||||
{/key}
|
||||
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
export type NavbarProject = {
|
||||
name: string;
|
||||
$id: string;
|
||||
region: string;
|
||||
isSelected: boolean;
|
||||
platformCount: number;
|
||||
pingCount: number;
|
||||
};
|
||||
|
||||
export type BaseNavbarProps = HTMLAttributes<HTMLHeadElement> & {
|
||||
logo: {
|
||||
src: string;
|
||||
@@ -106,7 +97,6 @@
|
||||
export let sideBarIsOpen: $$Props['sideBarIsOpen'] = false;
|
||||
export let showAccountMenu = false;
|
||||
export let currentProject: Models.Project = undefined;
|
||||
export let projects: Promise<Models.ProjectList> = undefined;
|
||||
|
||||
let activeTheme = $app.theme;
|
||||
let shouldAnimateThemeToggle = false;
|
||||
@@ -138,7 +128,9 @@
|
||||
class="only-desktop">
|
||||
<img src={logo.src} alt={logo.alt} />
|
||||
</a>
|
||||
<Breadcrumbs {organizations} {projects} {currentProject} />
|
||||
|
||||
<Breadcrumbs {organizations} {currentProject} />
|
||||
|
||||
{#if page.route?.id?.includes('/project-[region]-[project]') && currentProject && currentProject.pingCount === 0}
|
||||
<div class="only-desktop" style:margin-inline-start="-16px">
|
||||
<Button.Anchor
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
/**
|
||||
* Build VCS repo URL from the template response model.
|
||||
* Example (GitHub): https://github.com/appwrite/templates-for-sites
|
||||
*/
|
||||
export function getTemplateSourceUrl(
|
||||
t: Models.TemplateSite | Models.TemplateFunction
|
||||
): string | null {
|
||||
const owner = t.providerOwner;
|
||||
const repo = t.providerRepositoryId;
|
||||
const provider = t.vcsProvider; // e.g., "github"
|
||||
|
||||
if (!owner || !repo || !provider) return null;
|
||||
|
||||
// Map provider → host (extend if needed)
|
||||
const hostMap: Record<string, string> = {
|
||||
github: 'github.com',
|
||||
gitlab: 'gitlab.com',
|
||||
bitbucket: 'bitbucket.org'
|
||||
};
|
||||
|
||||
const host = hostMap[provider.toLowerCase()] ?? provider; // fallback
|
||||
|
||||
return `https://${host}/${owner}/${repo}`;
|
||||
}
|
||||
@@ -22,7 +22,6 @@
|
||||
export let showFooter = true;
|
||||
export let showSideNavigation = false;
|
||||
export let selectedProject: Models.Project = null;
|
||||
export let projects: Promise<Models.ProjectList> = undefined;
|
||||
|
||||
let yOnMenuOpen: number;
|
||||
let showContentTransition = false;
|
||||
@@ -104,8 +103,6 @@
|
||||
};
|
||||
}),
|
||||
|
||||
projects: projects,
|
||||
|
||||
currentProject: selectedProject
|
||||
};
|
||||
|
||||
|
||||
@@ -360,6 +360,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// mobile logo positioned at bottom in mobile view
|
||||
.mobile-logo {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
@@ -368,4 +369,16 @@
|
||||
max-width: 100%;
|
||||
margin-block-start: 0 !important;
|
||||
}
|
||||
|
||||
@media #{devices.$break1} {
|
||||
.auth-container {
|
||||
margin-block-end: 5rem;
|
||||
margin-block-start: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -320,8 +320,10 @@ export function calculateTrialDay(org: Organization) {
|
||||
export async function checkForProjectsLimit(org: Organization, orgProjectCount?: number) {
|
||||
if (!isCloud) return;
|
||||
if (!org) return;
|
||||
|
||||
const plan = await sdk.forConsole.billing.getOrganizationPlan(org.$id);
|
||||
if (!plan) return;
|
||||
|
||||
if (plan.$id !== BillingPlan.FREE) return;
|
||||
if (org.projects?.length > 0) return;
|
||||
|
||||
|
||||
@@ -55,8 +55,6 @@
|
||||
IconSwitchHorizontal
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
@@ -298,10 +296,8 @@
|
||||
if (isCloud) {
|
||||
currentOrganizationId = org.$id;
|
||||
const orgProjectCount =
|
||||
data.allProjects && data.currentOrgId === org.$id
|
||||
? data.allProjects.projects.length
|
||||
: undefined;
|
||||
checkForProjectsLimit(org, orgProjectCount);
|
||||
data.currentOrgId === org.$id ? data.allProjectsCount : undefined;
|
||||
await checkForProjectsLimit(org, orgProjectCount);
|
||||
checkForEnterpriseTrial(org);
|
||||
await checkForUsageLimit(org);
|
||||
checkForMarkedForDeletion(org);
|
||||
@@ -322,22 +318,6 @@
|
||||
|
||||
$: checkForUsageLimits($organization);
|
||||
|
||||
$: isOnOnboarding = page.route?.id?.includes('/(console)/onboarding');
|
||||
|
||||
$: projects = isOnOnboarding
|
||||
? null
|
||||
: sdk.forConsole.projects.list({
|
||||
queries: [
|
||||
Query.equal(
|
||||
'teamId',
|
||||
// id from page params ?? id from store ?? id from preferences
|
||||
page.params.organization ?? currentOrganizationId ?? data.currentOrgId
|
||||
),
|
||||
Query.limit(5),
|
||||
Query.orderDesc('$updatedAt')
|
||||
]
|
||||
});
|
||||
|
||||
$: if ($requestedMigration) {
|
||||
openMigrationWizard();
|
||||
}
|
||||
@@ -358,7 +338,6 @@
|
||||
!page.url.pathname.includes('/console/onboarding')}
|
||||
showHeader={!page.url.pathname.includes('/console/onboarding/create-project')}
|
||||
showFooter={!page.url.pathname.includes('/console/onboarding/create-project')}
|
||||
{projects}
|
||||
selectedProject={page.data?.project}>
|
||||
<!-- <Header slot="header" />-->
|
||||
<slot />
|
||||
|
||||
@@ -30,19 +30,17 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
|
||||
(organizations.teams.length > 0 ? organizations.teams[0].$id : undefined);
|
||||
|
||||
// Load projects for the current organization if one is selected
|
||||
let projects = null;
|
||||
let projectsCount = 0;
|
||||
if (currentOrgId) {
|
||||
try {
|
||||
projects = await sdk.forConsole.projects.list([
|
||||
Query.equal('teamId', currentOrgId),
|
||||
Query.limit(1000) // Get all projects for organization
|
||||
]);
|
||||
for (const project of projects.projects) {
|
||||
project.region ??= 'default';
|
||||
}
|
||||
projectsCount = (
|
||||
await sdk.forConsole.projects.list([
|
||||
Query.equal('teamId', currentOrgId),
|
||||
Query.limit(1)
|
||||
])
|
||||
).total;
|
||||
} catch (e) {
|
||||
// Handle error silently - projects might not be accessible
|
||||
projects = {};
|
||||
projectsCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +53,7 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
|
||||
organizations,
|
||||
consoleVariables,
|
||||
version: versionData?.version ?? null,
|
||||
allProjects: projects
|
||||
allProjectsCount: projectsCount
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -114,9 +114,8 @@
|
||||
trackError(e, Submit.ProjectCreate);
|
||||
}
|
||||
};
|
||||
onMount(async () => {
|
||||
checkPricingRefAndRedirect(page.url.searchParams);
|
||||
});
|
||||
|
||||
onMount(async () => checkPricingRefAndRedirect(page.url.searchParams));
|
||||
|
||||
function findRegion(project: Models.Project) {
|
||||
return $regionsStore.regions.find((region) => region.$id === project.region);
|
||||
@@ -140,7 +139,10 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<SelectProjectCloud selectedProjects={data.organization.projects || []} bind:showSelectProject />
|
||||
<SelectProjectCloud
|
||||
bind:showSelectProject
|
||||
organizationId={page.params.organization}
|
||||
selectedProjects={data.organization.projects || []} />
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export let teamId: string;
|
||||
export let showCreateProjectCloud: boolean;
|
||||
export let projects: number;
|
||||
export let showCreateProjectCloud: boolean;
|
||||
export let regions: Array<Models.ConsoleRegion> = [];
|
||||
|
||||
let id: string = null;
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
<slot name="framework-actions" />
|
||||
{/if}
|
||||
|
||||
{#if domain && showAfter}
|
||||
|
||||
+11
@@ -37,6 +37,7 @@
|
||||
import { connectGitHub } from '$lib/stores/git';
|
||||
import { getFrameworkIcon } from '$lib/stores/sites';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { getTemplateSourceUrl } from '$lib/helpers/templateSource';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -351,6 +352,16 @@
|
||||
alt={data.template.name}
|
||||
ratio="16/9" />
|
||||
</Layout.Stack>
|
||||
|
||||
<svelte:fragment slot="framework-actions">
|
||||
{@const sourceUrl = getTemplateSourceUrl(data.template)}
|
||||
{#if sourceUrl}
|
||||
<Button secondary size="s" external href={sourceUrl}>
|
||||
View source
|
||||
<Icon icon={IconExternalLink} slot="end" size="s" />
|
||||
</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Aside>
|
||||
</svelte:fragment>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user