This commit is contained in:
Mike 2024-08-06 13:39:58 +02:00 коммит произвёл GitHub
Родитель cd73a7aad8
Коммит ad1f97216f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
19 изменённых файлов: 429 добавлений и 3 удалений

Просмотреть файл

@ -39,3 +39,6 @@ NUXT_PUBLIC_SURVICATE_WORKSPACE_KEY=
# Enable direct preview image loading - way quicker, but requres server & frontend to be on the same origin
NUXT_PUBLIC_ENABLE_DIRECT_PREVIEWS=true
# Ghost API
NUXT_PUBLIC_GHOST_API_KEY=

Просмотреть файл

@ -4,7 +4,7 @@
:class="[
'transition grow px-3 inline-flex justify-center items-center outline-none h6 font-medium leading-7',
'rounded shadow bg-foundation- text-foreground dark:text-foreground-on-primary',
'border border-1 border-outline-2 hover:border-outline-3',
'border border-outline-2 hover:border-outline-3',
noVerticalPadding ? '' : 'py-2'
]"
>

Просмотреть файл

@ -0,0 +1,80 @@
<template>
<div
class="bg-foundation border border-outline-3 rounded-xl p-5 pb-4 flex flex-col gap-y-3"
>
<NuxtLink
:to="projectRoute(project.id)"
class="text-heading hover:text-primary truncate"
>
{{ project.name }}
</NuxtLink>
<div class="flex-1">
<p class="text-body-3xs text-foreground-2 capitalize">
{{ project.role?.split(':').reverse()[0] }}
<span class="pl-1 pr-2"></span>
<span v-tippy="updatedAt.full">
{{ updatedAt.relative }}
</span>
</p>
</div>
<UserAvatarGroup :users="teamUsers" :max-count="4" />
<div>
<FormButton
:to="allProjectModelsRoute(project.id)"
size="sm"
color="outline"
:icon-right="ChevronRightIcon"
>
{{
`${project.models.totalCount} ${
project.models.totalCount === 1 ? 'model' : 'models'
}`
}}
</FormButton>
</div>
</div>
</template>
<script lang="ts" setup>
import { graphql } from '~~/lib/common/generated/gql'
import type { DashboardProjectCard_ProjectFragment } from '~~/lib/common/generated/gql/graphql'
import { projectRoute, allProjectModelsRoute } from '~~/lib/common/helpers/route'
import { ChevronRightIcon } from '@heroicons/vue/20/solid'
import { useGeneralProjectPageUpdateTracking } from '~~/lib/projects/composables/projectPages'
graphql(`
fragment DashboardProjectCard_Project on Project {
id
name
role
updatedAt
models {
totalCount
}
team {
user {
...LimitedUserAvatar
}
}
}
`)
const props = defineProps<{
project: DashboardProjectCard_ProjectFragment
}>()
const projectId = computed(() => props.project.id)
useGeneralProjectPageUpdateTracking(
{ projectId },
{ redirectHomeOnProjectDeletion: false }
)
const teamUsers = computed(() => props.project.team.map((t) => t.user))
const updatedAt = computed(() => {
return {
full: formattedFullDate(props.project.updatedAt),
relative: formattedRelativeDate(props.project.updatedAt, { capitalize: true })
}
})
</script>

Просмотреть файл

@ -0,0 +1,41 @@
<template>
<NuxtLink :to="tutorial.url" target="_blank">
<div
class="bg-foundation border border-outline-3 rounded-xl flex flex-col overflow-hidden hover:border-outline-5 transition"
>
<div
:style="{ backgroundImage: `url(${tutorial.featureImage})` }"
class="bg-foundation-page bg-cover bg-center w-full h-32"
/>
<div class="p-5 pb-4">
<h3 v-if="tutorial.title" class="text-body-2xs text-foreground truncate">
{{ tutorial.title }}
</h3>
<p class="text-body-3xs text-foreground-2 capitalize mt-2">
<span v-tippy="updatedAt.full">
{{ updatedAt.relative }}
</span>
<template v-if="tutorial.readingTime">
<span class="pl-1 pr-2"></span>
{{ tutorial.readingTime }}m read
</template>
</p>
</div>
</div>
</NuxtLink>
</template>
<script lang="ts" setup>
import type { TutorialItem } from '~~/lib/dashboard/helpers/types'
const props = defineProps<{
tutorial: TutorialItem
}>()
const updatedAt = computed(() => {
return {
full: formattedFullDate(props.tutorial.publishedAt),
relative: formattedRelativeDate(props.tutorial.publishedAt, { capitalize: true })
}
})
</script>

Просмотреть файл

@ -0,0 +1,34 @@
<template>
<div class="border border-outline-3 rounded-lg p-5 pt-4">
<p class="text-heading-sm text-foreground">{{ title }}</p>
<p class="text-body-xs text-foreground-2 pt-1">{{ description }}</p>
<div
v-if="buttons"
class="flex flex-col flex-wrap md:flex-row gap-y-2 md:gap-x-2 gap-y-0 mt-3"
>
<FormButton
v-for="(button, index) in buttons"
:key="button.id || index"
v-bind="button.props || {}"
:disabled="button.props?.disabled || button.disabled"
:submit="button.props?.submit || button.submit"
size="sm"
color="outline"
@click="($event) => button.onClick?.($event)"
>
{{ button.text }}
</FormButton>
</div>
</div>
</template>
<script lang="ts" setup>
import { type LayoutDialogButton } from '@speckle/ui-components'
defineProps<{
title: string
description: string
buttons?: LayoutDialogButton[]
}>()
</script>

Просмотреть файл

@ -37,6 +37,7 @@ const documents = {
"\n fragment AutomateViewerPanel_AutomateRun on AutomateRun {\n id\n functionRuns {\n id\n ...AutomateViewerPanelFunctionRunRow_AutomateFunctionRun\n }\n ...AutomationsStatusOrderedRuns_AutomationRun\n }\n": types.AutomateViewerPanel_AutomateRunFragmentDoc,
"\n fragment AutomateViewerPanelFunctionRunRow_AutomateFunctionRun on AutomateFunctionRun {\n id\n results\n status\n statusMessage\n contextView\n function {\n id\n logo\n name\n }\n createdAt\n updatedAt\n }\n": types.AutomateViewerPanelFunctionRunRow_AutomateFunctionRunFragmentDoc,
"\n fragment CommonModelSelectorModel on Model {\n id\n name\n }\n": types.CommonModelSelectorModelFragmentDoc,
"\n fragment DashboardProjectCard_Project on Project {\n id\n name\n role\n updatedAt\n models {\n totalCount\n }\n team {\n user {\n ...LimitedUserAvatar\n }\n }\n }\n": types.DashboardProjectCard_ProjectFragmentDoc,
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": types.FormSelectModels_ModelFragmentDoc,
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": types.FormSelectProjects_ProjectFragmentDoc,
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": types.FormUsersSelectItemFragmentDoc,
@ -116,6 +117,7 @@ const documents = {
"\n query ServerInfoAllScopes {\n serverInfo {\n scopes {\n name\n description\n }\n }\n }\n": types.ServerInfoAllScopesDocument,
"\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectModelsSelectorValuesDocument,
"\n query MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n": types.MainServerInfoDataDocument,
"\n query DashboardProjectsPageQuery {\n activeUser {\n id\n projects(limit: 3) {\n items {\n ...DashboardProjectCard_Project\n }\n }\n }\n }\n": types.DashboardProjectsPageQueryDocument,
"\n mutation DeleteAccessToken($token: String!) {\n apiTokenRevoke(token: $token)\n }\n": types.DeleteAccessTokenDocument,
"\n mutation CreateAccessToken($token: ApiTokenCreateInput!) {\n apiTokenCreate(token: $token)\n }\n": types.CreateAccessTokenDocument,
"\n mutation DeleteApplication($appId: String!) {\n appDelete(appId: $appId)\n }\n": types.DeleteApplicationDocument,
@ -356,6 +358,10 @@ export function graphql(source: "\n fragment AutomateViewerPanelFunctionRunRow_
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment CommonModelSelectorModel on Model {\n id\n name\n }\n"): (typeof documents)["\n fragment CommonModelSelectorModel on Model {\n id\n name\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment DashboardProjectCard_Project on Project {\n id\n name\n role\n updatedAt\n models {\n totalCount\n }\n team {\n user {\n ...LimitedUserAvatar\n }\n }\n }\n"): (typeof documents)["\n fragment DashboardProjectCard_Project on Project {\n id\n name\n role\n updatedAt\n models {\n totalCount\n }\n team {\n user {\n ...LimitedUserAvatar\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@ -672,6 +678,10 @@ export function graphql(source: "\n query ProjectModelsSelectorValues($projectI
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n"): (typeof documents)["\n query MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query DashboardProjectsPageQuery {\n activeUser {\n id\n projects(limit: 3) {\n items {\n ...DashboardProjectCard_Project\n }\n }\n }\n }\n"): (typeof documents)["\n query DashboardProjectsPageQuery {\n activeUser {\n id\n projects(limit: 3) {\n items {\n ...DashboardProjectCard_Project\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

Просмотреть файл

@ -3980,6 +3980,8 @@ export type AutomateViewerPanelFunctionRunRow_AutomateFunctionRunFragment = { __
export type CommonModelSelectorModelFragment = { __typename?: 'Model', id: string, name: string };
export type DashboardProjectCard_ProjectFragment = { __typename?: 'Project', id: string, name: string, role?: string | null, updatedAt: string, models: { __typename?: 'ModelCollection', totalCount: number }, team: Array<{ __typename?: 'ProjectCollaborator', user: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null } }> };
export type FormSelectModels_ModelFragment = { __typename?: 'Model', id: string, name: string };
export type FormSelectProjects_ProjectFragment = { __typename?: 'Project', id: string, name: string };
@ -4237,6 +4239,11 @@ export type MainServerInfoDataQueryVariables = Exact<{ [key: string]: never; }>;
export type MainServerInfoDataQuery = { __typename?: 'Query', serverInfo: { __typename?: 'ServerInfo', adminContact?: string | null, blobSizeLimitBytes: number, canonicalUrl?: string | null, company?: string | null, description?: string | null, guestModeEnabled: boolean, inviteOnly?: boolean | null, name: string, termsOfService?: string | null, version?: string | null, automateUrl?: string | null } };
export type DashboardProjectsPageQueryQueryVariables = Exact<{ [key: string]: never; }>;
export type DashboardProjectsPageQueryQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', id: string, projects: { __typename?: 'ProjectCollection', items: Array<{ __typename?: 'Project', id: string, name: string, role?: string | null, updatedAt: string, models: { __typename?: 'ModelCollection', totalCount: number }, team: Array<{ __typename?: 'ProjectCollaborator', user: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null } }> }> } } | null };
export type DeleteAccessTokenMutationVariables = Exact<{
token: Scalars['String']['input'];
}>;
@ -5092,6 +5099,8 @@ export const AutomateFunctionsPageItems_QueryFragmentDoc = {"kind":"Document","d
export const AutomateViewerPanelFunctionRunRow_AutomateFunctionRunFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateViewerPanelFunctionRunRow_AutomateFunctionRun"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunctionRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"results"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"statusMessage"}},{"kind":"Field","name":{"kind":"Name","value":"contextView"}},{"kind":"Field","name":{"kind":"Name","value":"function"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<AutomateViewerPanelFunctionRunRow_AutomateFunctionRunFragment, unknown>;
export const AutomationsStatusOrderedRuns_AutomationRunFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomationsStatusOrderedRuns_AutomationRun"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"automation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"functionRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<AutomationsStatusOrderedRuns_AutomationRunFragment, unknown>;
export const AutomateViewerPanel_AutomateRunFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateViewerPanel_AutomateRun"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"functionRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateViewerPanelFunctionRunRow_AutomateFunctionRun"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomationsStatusOrderedRuns_AutomationRun"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateViewerPanelFunctionRunRow_AutomateFunctionRun"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunctionRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"results"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"statusMessage"}},{"kind":"Field","name":{"kind":"Name","value":"contextView"}},{"kind":"Field","name":{"kind":"Name","value":"function"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomationsStatusOrderedRuns_AutomationRun"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"automation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"functionRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<AutomateViewerPanel_AutomateRunFragment, unknown>;
export const LimitedUserAvatarFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]} as unknown as DocumentNode<LimitedUserAvatarFragment, unknown>;
export const DashboardProjectCard_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DashboardProjectCard_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]} as unknown as DocumentNode<DashboardProjectCard_ProjectFragment, unknown>;
export const FormSelectModels_ModelFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FormSelectModels_Model"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<FormSelectModels_ModelFragment, unknown>;
export const FormSelectProjects_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FormSelectProjects_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<FormSelectProjects_ProjectFragment, unknown>;
export const ProjectsPageTeamDialogManagePermissions_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsPageTeamDialogManagePermissions_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode<ProjectsPageTeamDialogManagePermissions_ProjectFragment, unknown>;
@ -5100,7 +5109,6 @@ export const HeaderNavShare_ProjectFragmentDoc = {"kind":"Document","definitions
export const ProjectModelPageHeaderProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectModelPageHeaderProject"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]} as unknown as DocumentNode<ProjectModelPageHeaderProjectFragment, unknown>;
export const ProjectPageProjectHeaderFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageProjectHeader"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}}]}}]} as unknown as DocumentNode<ProjectPageProjectHeaderFragment, unknown>;
export const PendingFileUploadFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PendingFileUpload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FileUpload"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"modelName"}},{"kind":"Field","name":{"kind":"Name","value":"convertedStatus"}},{"kind":"Field","name":{"kind":"Name","value":"convertedMessage"}},{"kind":"Field","name":{"kind":"Name","value":"uploadDate"}},{"kind":"Field","name":{"kind":"Name","value":"convertedLastUpdate"}},{"kind":"Field","name":{"kind":"Name","value":"fileType"}},{"kind":"Field","name":{"kind":"Name","value":"fileName"}}]}}]} as unknown as DocumentNode<PendingFileUploadFragment, unknown>;
export const LimitedUserAvatarFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]} as unknown as DocumentNode<LimitedUserAvatarFragment, unknown>;
export const ProjectModelPageDialogDeleteVersionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectModelPageDialogDeleteVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Version"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]} as unknown as DocumentNode<ProjectModelPageDialogDeleteVersionFragment, unknown>;
export const ProjectModelPageDialogMoveToVersionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectModelPageDialogMoveToVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Version"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]} as unknown as DocumentNode<ProjectModelPageDialogMoveToVersionFragment, unknown>;
export const FunctionRunStatusForSummaryFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FunctionRunStatusForSummary"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunctionRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]} as unknown as DocumentNode<FunctionRunStatusForSummaryFragment, unknown>;
@ -5203,6 +5211,7 @@ export const ServerInfoBlobSizeLimitDocument = {"kind":"Document","definitions":
export const ServerInfoAllScopesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ServerInfoAllScopes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]}}]} as unknown as DocumentNode<ServerInfoAllScopesQuery, ServerInfoAllScopesQueryVariables>;
export const ProjectModelsSelectorValuesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelsSelectorValues"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonModelSelectorModel"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CommonModelSelectorModel"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<ProjectModelsSelectorValuesQuery, ProjectModelsSelectorValuesQueryVariables>;
export const MainServerInfoDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MainServerInfoData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminContact"}},{"kind":"Field","name":{"kind":"Name","value":"blobSizeLimitBytes"}},{"kind":"Field","name":{"kind":"Name","value":"canonicalUrl"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"guestModeEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"inviteOnly"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"termsOfService"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"automateUrl"}}]}}]}}]} as unknown as DocumentNode<MainServerInfoDataQuery, MainServerInfoDataQueryVariables>;
export const DashboardProjectsPageQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DashboardProjectsPageQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"3"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"DashboardProjectCard_Project"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DashboardProjectCard_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}}]}}]}}]} as unknown as DocumentNode<DashboardProjectsPageQueryQuery, DashboardProjectsPageQueryQueryVariables>;
export const DeleteAccessTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteAccessToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiTokenRevoke"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}]}]}}]} as unknown as DocumentNode<DeleteAccessTokenMutation, DeleteAccessTokenMutationVariables>;
export const CreateAccessTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateAccessToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ApiTokenCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiTokenCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}]}]}}]} as unknown as DocumentNode<CreateAccessTokenMutation, CreateAccessTokenMutationVariables>;
export const DeleteApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"appDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}]}]}}]} as unknown as DocumentNode<DeleteApplicationMutation, DeleteApplicationMutationVariables>;

Просмотреть файл

@ -13,6 +13,8 @@ export const onboardingRoute = '/onboarding'
export const downloadManagerRoute = '/download-manager'
export const serverManagementRoute = '/server-management'
export const connectorsPageUrl = 'https://speckle.systems/features/connectors/'
export const docsPageUrl = 'https://speckle.guide/'
export const forumPageUrl = 'https://speckle.community/'
export const settingsQueries: {
[key: string]: {

Просмотреть файл

@ -0,0 +1,15 @@
export type ManagerExtension = 'exe' | 'dmg'
/* Util to download the Manager file */
export const downloadManager = (extension: ManagerExtension) => {
const fileName = `manager.${extension}`
const downloadLink = `https://releases.speckle.dev/manager2/installer/${fileName}`
const a = document.createElement('a')
a.style.display = 'none'
a.href = downloadLink
a.download = fileName
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}

Просмотреть файл

@ -0,0 +1,14 @@
import { graphql } from '~~/lib/common/generated/gql'
export const dashboardProjectsPageQuery = graphql(`
query DashboardProjectsPageQuery {
activeUser {
id
projects(limit: 3) {
items {
...DashboardProjectCard_Project
}
}
}
}
`)

Просмотреть файл

@ -0,0 +1,17 @@
import type { Nullable } from '@speckle/shared'
import { type LayoutDialogButton } from '@speckle/ui-components'
export type TutorialItem = {
id: string
readingTime?: number
publishedAt?: Nullable<string>
url?: string
title?: string
featureImage?: Nullable<string>
}
export type QuickStartItem = {
title: string
description: string
buttons: LayoutDialogButton[]
}

Просмотреть файл

@ -0,0 +1,28 @@
import type { Nullable } from '@speckle/shared'
/*
The Ghost API by default return huge images without any option to specify the format.
This can causes the images to take a long time to load.
Works around this issue by adding 'size' to the URL to request a smaller image
*/
export const getResizedGhostImage = ({
url,
width
}: {
url?: Nullable<string>
width: number
}): string | null => {
if (!url) return null
const pathParts = url.split('/')
const sizeSegment = `size/w${width}`
const imagesIndex = pathParts.indexOf('images')
if (imagesIndex !== -1) {
// Should always be there, but just the be sure in case it's an unexpected formatted URL
pathParts.splice(imagesIndex + 1, 0, sizeSegment)
url = pathParts.join('/')
}
return url
}

Просмотреть файл

@ -73,7 +73,8 @@ export default defineNuxtConfig({
datadogSite: '',
datadogService: '',
datadogEnv: '',
enableDirectPreviews: true
enableDirectPreviews: true,
ghostApiKey: ''
}
},

Просмотреть файл

@ -52,6 +52,7 @@
"@tiptap/pm": "2.0.0-beta.220",
"@tiptap/suggestion": "2.0.0-beta.220",
"@tiptap/vue-3": "2.0.0-beta.220",
"@tryghost/content-api": "^1.11.21",
"@vue/apollo-composable": "4.0.2",
"@vue/apollo-ssr": "4.0.0",
"@vueuse/core": "^10.9.0",
@ -109,6 +110,7 @@
"@types/mixpanel-browser": "^2.38.0",
"@types/node": "^18.17.5",
"@types/pino-http": "^5.8.1",
"@types/tryghost__content-api": "^1",
"@types/ua-parser-js": "^0.7.39",
"@typescript-eslint/eslint-plugin": "^7.12.0",
"@typescript-eslint/parser": "^7.12.0",

Просмотреть файл

@ -0,0 +1,140 @@
<template>
<div class="flex flex-col gap-y-12">
<section>
<h2 class="text-heading-sm text-foreground-2">Quick start</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 pt-5">
<QuickStartCard
v-for="quickStartItem in quickStartItems"
:key="quickStartItem.title"
:title="quickStartItem.title"
:description="quickStartItem.description"
:buttons="quickStartItem.buttons"
/>
</div>
</section>
<section>
<h2 class="text-heading-sm text-foreground-2">Recently updated projects</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 pt-5">
<DashboardProjectCard
v-for="project in projects"
:key="project.id"
:project="project"
/>
</div>
</section>
<section>
<h2 class="text-heading-sm text-foreground-2">News &amp; tutorials</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 pt-5">
<DashboardTutorialCard
v-for="tutorial in tutorials"
:key="tutorial.id"
:tutorial="tutorial"
/>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { dashboardProjectsPageQuery } from '~~/lib/dashboard/graphql/queries'
import type { QuickStartItem } from '~~/lib/dashboard/helpers/types'
import { getResizedGhostImage } from '~~/lib/dashboard/helpers/utils'
import { useQuery } from '@vue/apollo-composable'
import { useMixpanel } from '~~/lib/core/composables/mp'
import GhostContentAPI from '@tryghost/content-api'
import { docsPageUrl, forumPageUrl } from '~~/lib/common/helpers/route'
import type { ManagerExtension } from '~~/lib/common/utils/downloadManager'
import { downloadManager } from '~~/lib/common/utils/downloadManager'
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
useHead({ title: 'Dashboard' })
definePageMeta({
middleware: ['auth']
})
const config = useRuntimeConfig()
const mixpanel = useMixpanel()
const { result: projectsResult } = useQuery(dashboardProjectsPageQuery)
const { triggerNotification } = useGlobalToast()
const { data: tutorials } = await useLazyAsyncData('tutorials', fetchTutorials, {
server: false
})
const ghostContentApi = new GhostContentAPI({
url: 'https://speckle.systems',
key: config.public.ghostApiKey,
version: 'v5.0'
})
const quickStartItems = shallowRef<QuickStartItem[]>([
{
title: 'Install Speckle manager',
description: 'Use our Manager to install and manage Connectors with ease.',
buttons: [
{
text: 'Download for Windows',
onClick: () => onDownloadManager('exe')
},
{
text: 'Download for Mac',
onClick: () => onDownloadManager('dmg')
}
]
},
{
title: "Don't know where to start?",
description: "We'll walk you through some of most common usage scenarios.",
buttons: [
{
text: 'Open documentation',
props: { to: docsPageUrl, external: true }
}
]
},
{
title: 'Have a question you need answered?',
description: 'Submit your question on the forum and get help from the community.',
buttons: [
{
text: 'Ask a question',
props: { to: forumPageUrl, external: true }
}
]
}
])
const projects = computed(() => projectsResult.value?.activeUser?.projects.items)
async function fetchTutorials() {
const posts = await ghostContentApi.posts.browse({
limit: 8,
filter: 'visibility:public'
})
return posts
.filter((post) => post.url)
.map((post) => ({
id: post.id,
readingTime: post.reading_time,
publishedAt: post.published_at,
url: post.url,
title: post.title,
featureImage: getResizedGhostImage({ url: post.feature_image, width: 600 })
}))
}
const onDownloadManager = (extension: ManagerExtension) => {
try {
downloadManager(extension)
mixpanel.track('Manager Download', {
os: extension === 'exe' ? 'win' : 'mac'
})
} catch {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Download failed'
})
}
}
</script>

Просмотреть файл

@ -91,6 +91,10 @@ spec:
secretKeyRef:
name: {{ default .Values.secretName .Values.redis.connectionString.secretName }}
key: {{ default "redis_url" .Values.redis.connectionString.secretKey }}
{{- if .Values.frontend_2.ghostApiKey -}}
- name: NUXT_PUBLIC_GHOST_API_KEY
value: {{ .Values.frontend_2.ghostApiKey | quote }}
{{- end }}
{{- if .Values.analytics.datadog_app_id }}
- name: NUXT_PUBLIC_DATADOG_APP_ID
value: {{ .Values.analytics.datadog_app_id | quote }}

Просмотреть файл

@ -1504,6 +1504,11 @@
"description": "The Docker image to be used for the Speckle Frontend 2 component. If blank, defaults to speckle/speckle-frontend-2:{{ .Values.docker_image_tag }}. If provided, this value should be the full path including tag. The docker_image_tag value will be ignored.",
"default": ""
},
"ghostApiKey": {
"type": "string",
"description": "API Key for Ghost, which provides the blog content for the new web application frontend.",
"default": ""
},
"logClientApiToken": {
"type": "string",
"description": "SEQ API token",

Просмотреть файл

@ -950,6 +950,9 @@ frontend_2:
## @param frontend_2.image The Docker image to be used for the Speckle Frontend 2 component. If blank, defaults to speckle/speckle-frontend-2:{{ .Values.docker_image_tag }}. If provided, this value should be the full path including tag. The docker_image_tag value will be ignored.
##
image: ''
## @param frontend_2.ghostApiKey API Key for Ghost, which provides the blog content for the new web application frontend.
##
ghostApiKey: ''
## @param frontend_2.logClientApiToken SEQ API token
##
logClientApiToken: ''

Просмотреть файл

@ -15279,6 +15279,7 @@ __metadata:
"@tiptap/pm": "npm:2.0.0-beta.220"
"@tiptap/suggestion": "npm:2.0.0-beta.220"
"@tiptap/vue-3": "npm:2.0.0-beta.220"
"@tryghost/content-api": "npm:^1.11.21"
"@types/apollo-upload-client": "npm:^18.0.0"
"@types/dompurify": "npm:^3.0.2"
"@types/eslint": "npm:^8.56.10"
@ -15289,6 +15290,7 @@ __metadata:
"@types/mixpanel-browser": "npm:^2.38.0"
"@types/node": "npm:^18.17.5"
"@types/pino-http": "npm:^5.8.1"
"@types/tryghost__content-api": "npm:^1"
"@types/ua-parser-js": "npm:^0.7.39"
"@typescript-eslint/eslint-plugin": "npm:^7.12.0"
"@typescript-eslint/parser": "npm:^7.12.0"
@ -17834,6 +17836,15 @@ __metadata:
languageName: node
linkType: hard
"@tryghost/content-api@npm:^1.11.21":
version: 1.11.21
resolution: "@tryghost/content-api@npm:1.11.21"
dependencies:
axios: "npm:^1.0.0"
checksum: 10/5644bd7859e4fbbc0f36c9eb59fdff50646cf13e16d8547ae60760aff4db1c33f082b7bd5abf5c957e6f6457c206e3c59ed140a03050e5f5a631494ea66f4abc
languageName: node
linkType: hard
"@tryghost/content-api@npm:^1.5.12":
version: 1.11.19
resolution: "@tryghost/content-api@npm:1.11.19"
@ -19217,6 +19228,13 @@ __metadata:
languageName: node
linkType: hard
"@types/tryghost__content-api@npm:^1":
version: 1.3.16
resolution: "@types/tryghost__content-api@npm:1.3.16"
checksum: 10/6e63747e578ca97220560f10d293607599beb089720ed8967aa7e5aa0928a1e3f8d71ef711c49eef244afa828e63bf303854388ef47472285e777b209ee45265
languageName: node
linkType: hard
"@types/ua-parser-js@npm:^0.7.39":
version: 0.7.39
resolution: "@types/ua-parser-js@npm:0.7.39"