fix(automate): update empty state (#3642)
* fix(automate): wip onboarding copy * fix(automate): conditional onboarding buttons
This commit is contained in:
@@ -21,20 +21,25 @@
|
||||
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
|
||||
<div
|
||||
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"
|
||||
target="_blank"
|
||||
external
|
||||
size="sm"
|
||||
color="outline"
|
||||
@click="($event) => button.onClick?.($event)"
|
||||
v-tippy="button.disabledMessage"
|
||||
class="shrink-0"
|
||||
>
|
||||
{{ button.text }}
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-bind="button.props || {}"
|
||||
:disabled="button.props?.disabled || button.disabled"
|
||||
:submit="button.props?.submit || button.submit"
|
||||
target="_blank"
|
||||
external
|
||||
size="sm"
|
||||
color="outline"
|
||||
@click="($event) => button.onClick?.($event)"
|
||||
>
|
||||
{{ button.text }}
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-6 md:gap-y-8">
|
||||
<div class="p-4 flex flex-col gap-y-4 rounded-lg max-w-2xl mx-auto items-center">
|
||||
<div class="gap-y-4 flex flex-col items-center">
|
||||
<div class="text-heading-lg text-foreground">Scale your digital impact</div>
|
||||
<div class="text-body-xs text-foreground-2">
|
||||
Speckle Automate empowers you to continuously monitor your published models,
|
||||
automatically ensuring project data standards, identifying potential design
|
||||
faults, and effortlessly creating delivery artifacts.
|
||||
</div>
|
||||
<div class="flex gap-x-4">
|
||||
<FormButton
|
||||
:disabled="!!creationDisabledMessage"
|
||||
@click="$emit('new-automation')"
|
||||
>
|
||||
New automation
|
||||
</FormButton>
|
||||
<FormButton color="outline" :to="functionsGalleryRoute">
|
||||
View functions
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<section class="flex flex-col items-center justify-center py-8 md:py-16">
|
||||
<h3 class="text-heading-lg text-foreground">
|
||||
Scale your digital impact with Automate. Let's get you started...
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-3 pt-5 mt-4 max-w-5xl">
|
||||
<CommonCard
|
||||
v-for="emptyStateItem in emptyStateItems"
|
||||
:key="emptyStateItem.title"
|
||||
:title="emptyStateItem.title"
|
||||
:description="emptyStateItem.description"
|
||||
:buttons="emptyStateItem.buttons"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
@@ -29,20 +20,111 @@ import {
|
||||
workspaceFunctionsRoute
|
||||
} from '~/lib/common/helpers/route'
|
||||
import type { CreateAutomationSelectableFunction } from '~/lib/automate/helpers/automations'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
'new-automation': [fn?: CreateAutomationSelectableFunction]
|
||||
'new-function': []
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
workspaceSlug?: string
|
||||
isAutomateEnabled: boolean
|
||||
creationDisabledMessage?: string
|
||||
}>()
|
||||
export type AutomateOnboardingAction =
|
||||
| 'create-function'
|
||||
| 'view-functions'
|
||||
| 'create-automation'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
workspaceSlug?: string
|
||||
hiddenActions?: AutomateOnboardingAction[]
|
||||
disabledActions?: {
|
||||
action: AutomateOnboardingAction
|
||||
reason: string
|
||||
}[]
|
||||
}>(),
|
||||
{
|
||||
hiddenActions: () => [],
|
||||
disabledActions: () => []
|
||||
}
|
||||
)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const functionsGalleryRoute = computed(() =>
|
||||
props.workspaceSlug
|
||||
? workspaceFunctionsRoute(props.workspaceSlug)
|
||||
: automationFunctionsRoute
|
||||
)
|
||||
|
||||
const isVisibleAction = (action: LayoutDialogButton): boolean => {
|
||||
return !props.hiddenActions.includes(action.id as AutomateOnboardingAction)
|
||||
}
|
||||
|
||||
const isDisabledAction = (action: AutomateOnboardingAction): boolean => {
|
||||
return props.disabledActions.some((entry) => entry.action === action)
|
||||
}
|
||||
|
||||
const getDisabledMessage = (action: AutomateOnboardingAction): string | undefined => {
|
||||
return props.disabledActions.find((entry) => entry.action === action)?.reason
|
||||
}
|
||||
|
||||
const emptyStateItems = computed(() => {
|
||||
const items: {
|
||||
title: string
|
||||
description: string
|
||||
buttons: LayoutDialogButton[]
|
||||
}[] = [
|
||||
{
|
||||
title: "Capture your team's knowledge",
|
||||
description:
|
||||
'Turn tacit knowledge and monotonous process into code. Use private functions across projects in your workspace.',
|
||||
buttons: [
|
||||
{
|
||||
id: 'create-function',
|
||||
text: 'Create function',
|
||||
onClick: () => {
|
||||
emit('new-function')
|
||||
},
|
||||
disabled: isDisabledAction('create-function')
|
||||
},
|
||||
{
|
||||
id: 'view-functions',
|
||||
text: 'View functions',
|
||||
onClick: () => {
|
||||
router.push(functionsGalleryRoute.value)
|
||||
}
|
||||
}
|
||||
].filter(isVisibleAction)
|
||||
},
|
||||
{
|
||||
title: 'Automate your workflows',
|
||||
description:
|
||||
'Continuously ensure project data standards, generate delivery artifacts, and more!',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Create automation',
|
||||
onClick: () => {
|
||||
emit('new-automation')
|
||||
},
|
||||
disabled: isDisabledAction('create-automation'),
|
||||
disabledMessage: getDisabledMessage('create-automation')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Learn more',
|
||||
description:
|
||||
'Find out how Automate can be customised to support virtually any of your custom workflows.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'View docs',
|
||||
props: {
|
||||
to: 'https://speckle.guide/automate/',
|
||||
external: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
return items
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-model:search="search"
|
||||
:workspace-slug="workspaceSlug"
|
||||
:show-empty-state="shouldShowEmptyState"
|
||||
:creation-disabled-message="disableCreateMessage"
|
||||
:creation-disabled-message="disableCreateAutomationMessage"
|
||||
@new-automation="onNewAutomation"
|
||||
/>
|
||||
<template v-if="loading">
|
||||
@@ -14,10 +14,10 @@
|
||||
<ProjectPageAutomationsEmptyState
|
||||
v-if="shouldShowEmptyState"
|
||||
:workspace-slug="workspaceSlug"
|
||||
:functions="result"
|
||||
:is-automate-enabled="isAutomateEnabled"
|
||||
:creation-disabled-message="disableCreateMessage"
|
||||
:hidden-actions="hiddenActions"
|
||||
:disabled-actions="disabledActions"
|
||||
@new-automation="onNewAutomation"
|
||||
@new-function="onNewFunction"
|
||||
/>
|
||||
<template v-else>
|
||||
<template v-if="automations.length">
|
||||
@@ -43,6 +43,13 @@
|
||||
:preselected-project="project"
|
||||
:preselected-function="newAutomationTargetFn"
|
||||
/>
|
||||
<AutomateFunctionCreateDialog
|
||||
v-model:open="showNewFunctionDialog"
|
||||
:workspace-id="workspaceId"
|
||||
:is-authorized="isGithubAppConfigured"
|
||||
:github-orgs="githubOrgs"
|
||||
:templates="availableFunctionTemplates"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -54,6 +61,7 @@ import {
|
||||
import type { CreateAutomationSelectableFunction } from '~/lib/automate/helpers/automations'
|
||||
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import type { AutomateOnboardingAction } from '~/components/project/page/automations/EmptyState.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const projectId = computed(() => route.params.id as string)
|
||||
@@ -78,6 +86,58 @@ const { result, loading } = useQuery(
|
||||
const workspaceId = computed(() => result.value?.project?.workspace?.id)
|
||||
const workspaceSlug = computed(() => result.value?.project?.workspace?.slug)
|
||||
|
||||
const workspaceFunctionCount = computed(
|
||||
() => result.value?.project.workspace?.automateFunctions.totalCount ?? 0
|
||||
)
|
||||
const hiddenActions = computed<AutomateOnboardingAction[]>(() => {
|
||||
return workspaceFunctionCount.value > 0 ? [] : ['view-functions']
|
||||
})
|
||||
const disabledActions = computed<
|
||||
{ action: AutomateOnboardingAction; reason: string }[]
|
||||
>(() => {
|
||||
if (workspaceFunctionCount.value === 0) {
|
||||
return [
|
||||
{
|
||||
action: 'create-automation',
|
||||
reason:
|
||||
'You must create at least one function before you can create an automation.'
|
||||
}
|
||||
]
|
||||
}
|
||||
if (result.value?.project?.role !== Roles.Stream.Owner) {
|
||||
return [
|
||||
{
|
||||
action: 'create-automation',
|
||||
reason: 'Only project owners can create new automations.'
|
||||
}
|
||||
]
|
||||
}
|
||||
if ((result.value?.project?.models?.items.length || 0) === 0) {
|
||||
return [
|
||||
{
|
||||
action: 'create-automation',
|
||||
reason:
|
||||
'Your project should have at least 1 model before you can create an automation.'
|
||||
}
|
||||
]
|
||||
}
|
||||
return []
|
||||
})
|
||||
const disableCreateAutomationMessage = computed(
|
||||
() =>
|
||||
disabledActions.value?.find((entry) => entry.action === 'create-automation')?.reason
|
||||
)
|
||||
|
||||
const isGithubAppConfigured = computed(
|
||||
() => !!result.value?.activeUser?.automateInfo.hasAutomateGithubApp
|
||||
)
|
||||
const githubOrgs = computed(
|
||||
() => result.value?.activeUser?.automateInfo.availableGithubOrgs || []
|
||||
)
|
||||
const availableFunctionTemplates = computed(
|
||||
() => result.value?.serverInfo.automate.availableFunctionTemplates || []
|
||||
)
|
||||
|
||||
// Pagination query
|
||||
const {
|
||||
identifier,
|
||||
@@ -99,6 +159,7 @@ const {
|
||||
resolveCursorFromVariables: (vars) => vars.cursor
|
||||
})
|
||||
|
||||
const showNewFunctionDialog = ref(false)
|
||||
const showNewAutomationDialog = ref(false)
|
||||
const newAutomationTargetFn = ref<CreateAutomationSelectableFunction>()
|
||||
|
||||
@@ -118,22 +179,12 @@ const shouldShowEmptyState = computed(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
const disableCreateMessage = computed(() => {
|
||||
const allowedRoles: string[] = [Roles.Stream.Owner]
|
||||
|
||||
if (!allowedRoles.includes(result.value?.project?.role ?? '')) {
|
||||
return 'You must be a project owner to create automations.'
|
||||
}
|
||||
|
||||
if ((result.value?.project?.models?.items.length || 0) === 0) {
|
||||
return 'Your project should have at least 1 model before you can create an automation.'
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
const onNewAutomation = (fn?: CreateAutomationSelectableFunction) => {
|
||||
newAutomationTargetFn.value = fn
|
||||
showNewAutomationDialog.value = true
|
||||
}
|
||||
|
||||
const onNewFunction = () => {
|
||||
showNewFunctionDialog.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -258,7 +258,7 @@ const documents = {
|
||||
"\n query ProjectModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n ...ProjectModelPageVersionsPagination\n }\n }\n": types.ProjectModelVersionsDocument,
|
||||
"\n query ProjectModelsPage($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectModelsPageHeader_Project\n ...ProjectModelsPageResults_Project\n }\n }\n": types.ProjectModelsPageDocument,
|
||||
"\n query ProjectDiscussionsPage($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectDiscussionsPageHeader_Project\n ...ProjectDiscussionsPageResults_Project\n }\n }\n": types.ProjectDiscussionsPageDocument,
|
||||
"\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n slug\n }\n ...FormSelectProjects_Project\n }\n }\n": types.ProjectAutomationsTabDocument,
|
||||
"\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n slug\n automateFunctions(limit: 0) {\n totalCount\n }\n }\n ...FormSelectProjects_Project\n }\n ...AutomateFunctionsPageHeader_Query\n }\n": types.ProjectAutomationsTabDocument,
|
||||
"\n query ProjectAutomationsTabAutomationsPagination(\n $projectId: String!\n $search: String = null\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n automations(filter: $search, cursor: $cursor, limit: 5) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n }\n }\n }\n": types.ProjectAutomationsTabAutomationsPaginationDocument,
|
||||
"\n query ProjectAutomationPage($projectId: String!, $automationId: String!) {\n project(id: $projectId) {\n id\n ...ProjectPageAutomationPage_Project\n automation(id: $automationId) {\n id\n ...ProjectPageAutomationPage_Automation\n }\n }\n }\n": types.ProjectAutomationPageDocument,
|
||||
"\n query ProjectAutomationPagePaginatedRuns(\n $projectId: String!\n $automationId: String!\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n automation(id: $automationId) {\n id\n runs(cursor: $cursor, limit: 10) {\n totalCount\n cursor\n items {\n id\n ...AutomationRunDetails\n }\n }\n }\n }\n }\n": types.ProjectAutomationPagePaginatedRunsDocument,
|
||||
@@ -1373,7 +1373,7 @@ export function graphql(source: "\n query ProjectDiscussionsPage($projectId: St
|
||||
/**
|
||||
* 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 ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n slug\n }\n ...FormSelectProjects_Project\n }\n }\n"): (typeof documents)["\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n slug\n }\n ...FormSelectProjects_Project\n }\n }\n"];
|
||||
export function graphql(source: "\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n slug\n automateFunctions(limit: 0) {\n totalCount\n }\n }\n ...FormSelectProjects_Project\n }\n ...AutomateFunctionsPageHeader_Query\n }\n"): (typeof documents)["\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n slug\n automateFunctions(limit: 0) {\n totalCount\n }\n }\n ...FormSelectProjects_Project\n }\n ...AutomateFunctionsPageHeader_Query\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -254,9 +254,13 @@ export const projectAutomationsTabQuery = graphql(`
|
||||
workspace {
|
||||
id
|
||||
slug
|
||||
automateFunctions(limit: 0) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
...FormSelectProjects_Project
|
||||
}
|
||||
...AutomateFunctionsPageHeader_Query
|
||||
}
|
||||
`)
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export type LayoutDialogButton = {
|
||||
props?: Record<string, unknown> & FormButtonProps
|
||||
onClick?: (e: MouseEvent) => void
|
||||
disabled?: boolean
|
||||
disabledMessage?: string
|
||||
submit?: boolean
|
||||
/**
|
||||
* This should uniquely identify the button within the form. Even if you have different sets
|
||||
|
||||
Reference in New Issue
Block a user