Merge branch 'main' into fabians/core-ioc-85
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
:on-submit="onSubmit"
|
||||
max-width="md"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-body-xs text-foreground font-medium">
|
||||
How can we improve Speckle? If you have a feature request, please also share how
|
||||
you would use it and why it's important to you
|
||||
@@ -18,6 +18,13 @@
|
||||
label="Feedback"
|
||||
color="foundation"
|
||||
/>
|
||||
<p class="text-body-xs !leading-4">
|
||||
Need help? For support, head over to our
|
||||
<FormButton to="https://speckle.community/" target="_blank" link text>
|
||||
community forum
|
||||
</FormButton>
|
||||
where we can chat and solve problems together.
|
||||
</p>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
viewBox="0 0 8 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-outline-2"
|
||||
class="text-outline-5"
|
||||
>
|
||||
<path d="M2 18L6 6" stroke="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
<NuxtLink
|
||||
:to="disableLink ? undefined : to"
|
||||
class="flex gap-1 items-center text-body-xs ml-0.5 text-foreground-2 select-none truncate font-medium"
|
||||
class="flex gap-1 items-center text-body-xs ml-0.5 text-foreground-2 select-none truncate"
|
||||
:class="disableLink ? '' : 'hover:!text-foreground'"
|
||||
active-class="group is-active !text-foreground"
|
||||
active-class="group is-active !text-foreground font-medium"
|
||||
>
|
||||
<div class="truncate">
|
||||
{{ name || to }}
|
||||
|
||||
@@ -98,7 +98,7 @@ const token = computed(
|
||||
)
|
||||
const mainClasses = computed(() => {
|
||||
const classParts = [
|
||||
'flex flex-col space-y-4 px-4 py-5 transition border-x border-b border-outline-2 first:border-t first:rounded-t-lg last:rounded-b-lg'
|
||||
'flex flex-col space-y-4 px-4 py-5 transition bg-foundation border-x border-b border-outline-2 first:border-t first:rounded-t-lg last:rounded-b-lg'
|
||||
]
|
||||
|
||||
if (props.block) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="hasBanners"
|
||||
class="bg-foundation divide-y divide-outline-3 mb-8 empty:mb-0"
|
||||
>
|
||||
<div v-if="hasBanners" class="space-y-3 mb-8 empty:mb-0">
|
||||
<ProjectsInviteBanners
|
||||
v-if="projectsInvites?.projectInvites?.length"
|
||||
:invites="projectsInvites"
|
||||
|
||||
@@ -161,11 +161,11 @@ const showActionsMenu = ref<Record<string, boolean>>({})
|
||||
const actionItems: LayoutMenuItem[][] = [
|
||||
[
|
||||
{
|
||||
title: 'Change role',
|
||||
title: 'Change role...',
|
||||
id: ActionTypes.ChangeRole
|
||||
},
|
||||
{
|
||||
title: 'Remove user',
|
||||
title: 'Remove user...',
|
||||
id: ActionTypes.RemoveUser
|
||||
}
|
||||
]
|
||||
|
||||
@@ -129,7 +129,7 @@ const invites = computed(() => result.value?.admin.inviteList.items || [])
|
||||
const actionItems: LayoutMenuItem[][] = [
|
||||
[
|
||||
{ title: 'Resend invitation', id: 'resend-invite' },
|
||||
{ title: 'Delete invitation', id: 'delete-invite' }
|
||||
{ title: 'Delete invitation...', id: 'delete-invite' }
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@@ -177,8 +177,8 @@ const showActionsMenu = ref<Record<string, boolean>>({})
|
||||
|
||||
const actionItems: LayoutMenuItem[][] = [
|
||||
[
|
||||
{ title: 'View project...', id: ActionTypes.ViewProject },
|
||||
{ title: 'Edit members...', id: ActionTypes.EditMembers },
|
||||
{ title: 'View project', id: ActionTypes.ViewProject },
|
||||
{ title: 'Edit members', id: ActionTypes.EditMembers },
|
||||
{ title: 'Remove project...', id: ActionTypes.RemoveProject }
|
||||
]
|
||||
]
|
||||
|
||||
@@ -290,6 +290,8 @@ const updateWorkspaceSlug = async (newSlug: string) => {
|
||||
title: 'Workspace short ID updated'
|
||||
})
|
||||
|
||||
showEditSlugDialog.value = false
|
||||
|
||||
slug.value = newSlug
|
||||
|
||||
if (route.params.slug === oldSlug) {
|
||||
|
||||
@@ -5,23 +5,36 @@
|
||||
max-width="sm"
|
||||
:buttons="dialogButtons"
|
||||
>
|
||||
<p class="text-body-xs text-foreground">
|
||||
Are you sure you want to
|
||||
<span class="font-medium">permanently delete</span>
|
||||
the selected workspace?
|
||||
</p>
|
||||
<div
|
||||
class="rounded border bg-foundation-2 border-outline-3 text-body-2xs text-foreground font-medium py-3 px-4 my-4"
|
||||
>
|
||||
{{ workspace.name }}
|
||||
</div>
|
||||
<p class="text-body-xs text-foreground">
|
||||
This action
|
||||
<span class="font-medium">cannot</span>
|
||||
be undone.
|
||||
<p class="text-body-xs text-foreground mb-2">
|
||||
Are you sure you want to permanently delete
|
||||
<span class="font-medium">{{ workspace.name }}?</span>
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<FormTextInput
|
||||
v-model="workspaceNameInput"
|
||||
name="workspaceNameConfirm"
|
||||
label="To confirm deletion, type the workspace name below."
|
||||
placeholder="Type the workspace name here..."
|
||||
full-width
|
||||
show-label
|
||||
hide-error-message
|
||||
class="text-sm mb-2"
|
||||
color="foundation"
|
||||
/>
|
||||
<FormTextArea
|
||||
v-model="feedback"
|
||||
name="reasonForDeletion"
|
||||
label="Why did you delete this workspace?"
|
||||
placeholder="We want to improve so we're curious about your honest feedback"
|
||||
show-label
|
||||
show-optional
|
||||
full-width
|
||||
class="text-sm mb-2"
|
||||
color="foundation"
|
||||
/>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type {
|
||||
@@ -29,7 +42,7 @@ import type {
|
||||
UserWorkspacesArgs,
|
||||
User
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { FormTextInput, type LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { useMutation, useApolloClient } from '@vue/apollo-composable'
|
||||
import { deleteWorkspaceMutation } from '~/lib/settings/graphql/mutations'
|
||||
import {
|
||||
@@ -43,6 +56,8 @@ import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
import { isUndefined } from 'lodash-es'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { homeRoute } from '~/lib/common/helpers/route'
|
||||
import { useZapier } from '~/lib/core/composables/zapier'
|
||||
import { useForm } from 'vee-validate'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspaceGeneralDeleteDialog_Workspace on Workspace {
|
||||
@@ -63,10 +78,14 @@ const { activeUser } = useActiveUser()
|
||||
const router = useRouter()
|
||||
const apollo = useApolloClient().client
|
||||
const mixpanel = useMixpanel()
|
||||
const { sendWebhook } = useZapier()
|
||||
const { resetForm } = useForm<{ feedback: string }>()
|
||||
|
||||
const workspaceNameInput = ref('')
|
||||
const feedback = ref('')
|
||||
|
||||
const onDelete = async () => {
|
||||
router.push(homeRoute)
|
||||
isOpen.value = false
|
||||
if (workspaceNameInput.value !== props.workspace.name) return
|
||||
|
||||
const cache = apollo.cache
|
||||
const result = await deleteWorkspace({
|
||||
@@ -98,16 +117,30 @@ const onDelete = async () => {
|
||||
)
|
||||
}
|
||||
|
||||
mixpanel.track('Workspace Deleted', {
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: props.workspace.id,
|
||||
feedback: feedback.value
|
||||
})
|
||||
|
||||
// Only send zapier-discord webhook if not in dev environment
|
||||
if (!import.meta.dev) {
|
||||
await sendWebhook('https://hooks.zapier.com/hooks/catch/12120532/2m4okri/', {
|
||||
userId: activeUser.value?.id ?? '',
|
||||
feedback: feedback.value
|
||||
? `Action: Workspace Deleted(${props.workspace.name}) Feedback: ${feedback.value}`
|
||||
: `Action: Workspace Deleted(${props.workspace.name}) - No feedback provided`
|
||||
})
|
||||
}
|
||||
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Success,
|
||||
title: 'Workspace deleted',
|
||||
description: `The ${props.workspace.name} workspace has been deleted`
|
||||
})
|
||||
|
||||
mixpanel.track('Workspace Deleted', {
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: props.workspace.id
|
||||
})
|
||||
router.push(homeRoute)
|
||||
isOpen.value = false
|
||||
} else {
|
||||
const errorMessage = getFirstErrorMessage(result?.errors)
|
||||
triggerNotification({
|
||||
@@ -129,9 +162,14 @@ const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Delete',
|
||||
props: {
|
||||
color: 'danger'
|
||||
color: 'danger',
|
||||
disabled: workspaceNameInput.value !== props.workspace.name
|
||||
},
|
||||
onClick: onDelete
|
||||
}
|
||||
])
|
||||
|
||||
watch(isOpen, () => {
|
||||
resetForm()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -19,11 +19,16 @@
|
||||
<FormTextInput
|
||||
v-model:model-value="workspaceShortId"
|
||||
name="slug"
|
||||
label="Short ID"
|
||||
label="New short ID"
|
||||
:help="`${baseUrl}${workspaceRoute(workspaceShortId)}`"
|
||||
color="foundation"
|
||||
:rules="[isStringOfLength({ maxLength: 50, minLength: 3 }), isValidWorkspaceSlug]"
|
||||
:rules="[isStringOfLength({ maxLength: 50, minLength: 3 })]"
|
||||
:custom-error-message="
|
||||
workspaceShortId !== originalSlug ? error?.graphQLErrors[0]?.message : undefined
|
||||
"
|
||||
:loading="loading"
|
||||
show-label
|
||||
@update:model-value="updateDebouncedShortId"
|
||||
/>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
@@ -33,11 +38,11 @@ import { useForm } from 'vee-validate'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import type { SettingsWorkspacesGeneralEditSlugDialog_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
isStringOfLength,
|
||||
isValidWorkspaceSlug
|
||||
} from '~~/lib/common/helpers/validation'
|
||||
import { isStringOfLength } from '~~/lib/common/helpers/validation'
|
||||
import { workspaceRoute } from '~/lib/common/helpers/route'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { validateWorkspaceSlugQuery } from '~/lib/workspaces/graphql/queries'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesGeneralEditSlugDialog_Workspace on Workspace {
|
||||
@@ -57,13 +62,27 @@ const emit = defineEmits<{
|
||||
(e: 'update:slug', newSlug: string): void
|
||||
}>()
|
||||
|
||||
const { handleSubmit } = useForm<{ slug: string }>()
|
||||
|
||||
// Main ref that holds the current value of the slug input.
|
||||
const workspaceShortId = ref(props.workspace.slug)
|
||||
// Used to debounce API calls for slug validation.
|
||||
const debouncedWorkspaceShortId = ref(props.workspace.slug)
|
||||
// Keeps track of the initially generated slug to prevent unnecessary validations.
|
||||
const originalSlug = ref(props.workspace.slug)
|
||||
|
||||
const { error, loading } = useQuery(
|
||||
validateWorkspaceSlugQuery,
|
||||
() => ({
|
||||
slug: debouncedWorkspaceShortId.value
|
||||
}),
|
||||
() => ({
|
||||
enabled: debouncedWorkspaceShortId.value !== props.workspace.slug
|
||||
})
|
||||
)
|
||||
|
||||
const { handleSubmit, resetForm } = useForm<{ slug: string }>()
|
||||
|
||||
const updateSlug = handleSubmit(() => {
|
||||
emit('update:slug', workspaceShortId.value)
|
||||
isOpen.value = false
|
||||
})
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
@@ -78,12 +97,16 @@ const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
text: 'Update',
|
||||
props: {
|
||||
color: 'primary',
|
||||
disabled: workspaceShortId.value === props.workspace.slug
|
||||
disabled: workspaceShortId.value === props.workspace.slug || error.value !== null
|
||||
},
|
||||
submit: true
|
||||
}
|
||||
])
|
||||
|
||||
const updateDebouncedShortId = debounce((value: string) => {
|
||||
debouncedWorkspaceShortId.value = value
|
||||
}, 300)
|
||||
|
||||
watch(
|
||||
() => props.workspace.slug,
|
||||
(newValue) => {
|
||||
@@ -91,4 +114,14 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => isOpen.value,
|
||||
(newValue) => {
|
||||
if (!newValue) {
|
||||
resetForm()
|
||||
error.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -23,12 +23,11 @@
|
||||
label="Short ID"
|
||||
:help="getShortIdHelp"
|
||||
color="foundation"
|
||||
:rules="[
|
||||
isStringOfLength({ maxLength: 50, minLength: 3 }),
|
||||
isValidWorkspaceSlug
|
||||
]"
|
||||
:loading="loading"
|
||||
:rules="isStringOfLength({ maxLength: 50, minLength: 3 })"
|
||||
:custom-error-message="error?.graphQLErrors[0]?.message"
|
||||
show-label
|
||||
@update:model-value="shortIdManuallyEdited = true"
|
||||
@update:model-value="onSlugChange"
|
||||
/>
|
||||
<UserAvatarEditable
|
||||
v-model:edit-mode="editAvatarMode"
|
||||
@@ -49,13 +48,11 @@ import type { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { useCreateWorkspace } from '~/lib/workspaces/composables/management'
|
||||
import { useWorkspacesAvatar } from '~/lib/workspaces/composables/avatar'
|
||||
import {
|
||||
isRequired,
|
||||
isStringOfLength,
|
||||
isValidWorkspaceSlug
|
||||
} from '~~/lib/common/helpers/validation'
|
||||
import { isRequired, isStringOfLength } from '~~/lib/common/helpers/validation'
|
||||
import { generateSlugFromName } from '@speckle/shared'
|
||||
import { debounce } from 'lodash'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { validateWorkspaceSlugQuery } from '~/lib/workspaces/graphql/queries'
|
||||
|
||||
const emit = defineEmits<(e: 'created') => void>()
|
||||
|
||||
@@ -69,15 +66,25 @@ const isOpen = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const createWorkspace = useCreateWorkspace()
|
||||
const { generateDefaultLogoIndex, getDefaultAvatar } = useWorkspacesAvatar()
|
||||
const { handleSubmit } = useForm<{ name: string; slug: string }>()
|
||||
const { handleSubmit, resetForm } = useForm<{ name: string; slug: string }>()
|
||||
|
||||
const workspaceName = ref('')
|
||||
const workspaceShortId = ref('')
|
||||
const debouncedWorkspaceShortId = ref('')
|
||||
const editAvatarMode = ref(false)
|
||||
const workspaceLogo = ref<MaybeNullOrUndefined<string>>()
|
||||
const defaultLogoIndex = ref(0)
|
||||
const shortIdManuallyEdited = ref(false)
|
||||
const customShortIdError = ref('')
|
||||
|
||||
const { error, loading } = useQuery(
|
||||
validateWorkspaceSlugQuery,
|
||||
() => ({
|
||||
slug: debouncedWorkspaceShortId.value
|
||||
}),
|
||||
() => ({
|
||||
enabled: !!debouncedWorkspaceShortId.value
|
||||
})
|
||||
)
|
||||
|
||||
const baseUrl = useRuntimeConfig().public.baseUrl
|
||||
|
||||
@@ -105,7 +112,7 @@ const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
disabled:
|
||||
!workspaceName.value.trim() ||
|
||||
!workspaceShortId.value.trim() ||
|
||||
!!customShortIdError.value
|
||||
error.value !== null
|
||||
}
|
||||
}
|
||||
])
|
||||
@@ -122,7 +129,7 @@ const handleCreateWorkspace = handleSubmit(async () => {
|
||||
{ source: props.eventSource }
|
||||
)
|
||||
|
||||
if (newWorkspace) {
|
||||
if (newWorkspace && !newWorkspace?.errors) {
|
||||
emit('created')
|
||||
isOpen.value = false
|
||||
}
|
||||
@@ -135,21 +142,36 @@ const onLogoSave = (newVal: MaybeNullOrUndefined<string>) => {
|
||||
|
||||
const reset = () => {
|
||||
defaultLogoIndex.value = generateDefaultLogoIndex()
|
||||
workspaceName.value = ''
|
||||
workspaceShortId.value = ''
|
||||
debouncedWorkspaceShortId.value = ''
|
||||
workspaceLogo.value = null
|
||||
editAvatarMode.value = false
|
||||
shortIdManuallyEdited.value = false
|
||||
customShortIdError.value = ''
|
||||
error.value = null
|
||||
}
|
||||
|
||||
const updateShortId = debounce((newName: string) => {
|
||||
if (!shortIdManuallyEdited.value) {
|
||||
workspaceShortId.value = generateSlugFromName({ name: newName })
|
||||
const newSlug = generateSlugFromName({ name: newName })
|
||||
workspaceShortId.value = newSlug
|
||||
updateDebouncedShortId(newSlug)
|
||||
}
|
||||
}, 600)
|
||||
|
||||
const updateDebouncedShortId = debounce((newSlug: string) => {
|
||||
debouncedWorkspaceShortId.value = newSlug
|
||||
}, 300)
|
||||
|
||||
const onSlugChange = (newSlug: string) => {
|
||||
workspaceShortId.value = newSlug
|
||||
shortIdManuallyEdited.value = true
|
||||
updateDebouncedShortId(newSlug)
|
||||
}
|
||||
|
||||
// Seperate resets to avoid a temporary invalid state on submission
|
||||
watch(isOpen, (newVal) => {
|
||||
if (newVal) reset()
|
||||
if (!newVal) {
|
||||
reset()
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<span
|
||||
v-tippy="
|
||||
project.role !== Roles.Stream.Owner &&
|
||||
'Only project owners can move projects'
|
||||
'Only the project owner can move this project'
|
||||
"
|
||||
>
|
||||
<FormButton
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="py-4 text-body-xs text-foreground-2">
|
||||
You don't have any projects that are moveable to this workspace
|
||||
You don't have any projects that can be moved into this workspace
|
||||
</p>
|
||||
|
||||
<ProjectsMoveToWorkspaceDialog
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="w-36 flex flex-col items-center md:mx-4">
|
||||
<div class="w-40 flex flex-col items-center md:mx-4">
|
||||
<CommonProgressBar
|
||||
class="mb-1"
|
||||
:current-value="versionsCount.current"
|
||||
|
||||
@@ -325,6 +325,7 @@ const documents = {
|
||||
"\n query WorkspaceProjectsQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceProjectList_ProjectCollection\n }\n }\n }\n": types.WorkspaceProjectsQueryDocument,
|
||||
"\n query WorkspaceInvite(\n $workspaceId: String\n $token: String\n $options: WorkspaceInviteLookupOptions\n ) {\n workspaceInvite(workspaceId: $workspaceId, token: $token, options: $options) {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n ...WorkspaceInviteBlock_PendingWorkspaceCollaborator\n }\n }\n": types.WorkspaceInviteDocument,
|
||||
"\n query MoveProjectsDialog {\n activeUser {\n ...MoveProjectsDialog_User\n }\n }\n": types.MoveProjectsDialogDocument,
|
||||
"\n query ValidateWorkspaceSlug($slug: String!) {\n validateWorkspaceSlug(slug: $slug)\n }\n": types.ValidateWorkspaceSlugDocument,
|
||||
"\n query LegacyBranchRedirectMetadata($streamId: String!, $branchName: String!) {\n project(id: $streamId) {\n modelByName(name: $branchName) {\n id\n }\n }\n }\n": types.LegacyBranchRedirectMetadataDocument,
|
||||
"\n query LegacyViewerCommitRedirectMetadata($streamId: String!, $commitId: String!) {\n project(id: $streamId) {\n version(id: $commitId) {\n id\n model {\n id\n }\n }\n }\n }\n": types.LegacyViewerCommitRedirectMetadataDocument,
|
||||
"\n query LegacyViewerStreamRedirectMetadata($streamId: String!) {\n project(id: $streamId) {\n id\n versions(limit: 1) {\n totalCount\n items {\n id\n model {\n id\n }\n }\n }\n }\n }\n": types.LegacyViewerStreamRedirectMetadataDocument,
|
||||
@@ -1601,6 +1602,10 @@ export function graphql(source: "\n query WorkspaceInvite(\n $workspaceId: S
|
||||
* 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 MoveProjectsDialog {\n activeUser {\n ...MoveProjectsDialog_User\n }\n }\n"): (typeof documents)["\n query MoveProjectsDialog {\n activeUser {\n ...MoveProjectsDialog_User\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 ValidateWorkspaceSlug($slug: String!) {\n validateWorkspaceSlug(slug: $slug)\n }\n"): (typeof documents)["\n query ValidateWorkspaceSlug($slug: String!) {\n validateWorkspaceSlug(slug: $slug)\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
@@ -72,3 +72,9 @@ export const moveProjectsDialogQuery = graphql(`
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const validateWorkspaceSlugQuery = graphql(`
|
||||
query ValidateWorkspaceSlug($slug: String!) {
|
||||
validateWorkspaceSlug(slug: $slug)
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -171,10 +171,10 @@ const actionsItems = computed<LayoutMenuItem[][]>(() => {
|
||||
if (isWorkspacesEnabled.value && !project.value?.workspace?.id) {
|
||||
items.push([
|
||||
{
|
||||
title: 'Move...',
|
||||
title: 'Move project...',
|
||||
id: ActionTypes.Move,
|
||||
disabled: !isOwner.value,
|
||||
disabledTooltip: 'Only project owners can move projects into workspaces'
|
||||
disabledTooltip: 'Only the project owner can move this project into a workspace'
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ extend type Project {
|
||||
"""
|
||||
Retrieve a specific project version by its ID
|
||||
"""
|
||||
version(id: String!): Version
|
||||
version(id: String!): Version!
|
||||
|
||||
"""
|
||||
Retrieve a specific project model by its ID
|
||||
|
||||
@@ -1836,7 +1836,7 @@ export type Project = {
|
||||
team: Array<ProjectCollaborator>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
/** Retrieve a specific project version by its ID */
|
||||
version?: Maybe<Version>;
|
||||
version: Version;
|
||||
/** Returns a flat list of all project versions */
|
||||
versions: VersionCollection;
|
||||
/** Return metadata about resources being requested in the viewer */
|
||||
@@ -5508,7 +5508,7 @@ export type ProjectResolvers<ContextType = GraphQLContext, ParentType extends Re
|
||||
sourceApps?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
team?: Resolver<Array<ResolversTypes['ProjectCollaborator']>, ParentType, ContextType>;
|
||||
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
version?: Resolver<Maybe<ResolversTypes['Version']>, ParentType, ContextType, RequireFields<ProjectVersionArgs, 'id'>>;
|
||||
version?: Resolver<ResolversTypes['Version'], ParentType, ContextType, RequireFields<ProjectVersionArgs, 'id'>>;
|
||||
versions?: Resolver<ResolversTypes['VersionCollection'], ParentType, ContextType, RequireFields<ProjectVersionsArgs, 'limit'>>;
|
||||
viewerResources?: Resolver<Array<ResolversTypes['ViewerResourceGroup']>, ParentType, ContextType, RequireFields<ProjectViewerResourcesArgs, 'loadedVersionsOnly' | 'resourceIdString'>>;
|
||||
visibility?: Resolver<ResolversTypes['ProjectVisibility'], ParentType, ContextType>;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
batchDeleteCommits,
|
||||
batchMoveCommitsFactory
|
||||
} from '@/modules/core/services/commit/batchCommitActions'
|
||||
import { CommitUpdateError } from '@/modules/core/errors/commit'
|
||||
import { CommitNotFoundError, CommitUpdateError } from '@/modules/core/errors/commit'
|
||||
import {
|
||||
createCommitByBranchIdFactory,
|
||||
markCommitReceivedAndNotify,
|
||||
@@ -106,9 +106,14 @@ const batchMoveCommits = batchMoveCommitsFactory({
|
||||
export = {
|
||||
Project: {
|
||||
async version(parent, args, ctx) {
|
||||
return await ctx.loaders.streams.getStreamCommit
|
||||
const version = await ctx.loaders.streams.getStreamCommit
|
||||
.forStream(parent.id)
|
||||
.load(args.id)
|
||||
if (!version) {
|
||||
throw new CommitNotFoundError('Version not found')
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
},
|
||||
Version: {
|
||||
|
||||
@@ -1820,7 +1820,7 @@ export type Project = {
|
||||
team: Array<ProjectCollaborator>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
/** Retrieve a specific project version by its ID */
|
||||
version?: Maybe<Version>;
|
||||
version: Version;
|
||||
/** Returns a flat list of all project versions */
|
||||
versions: VersionCollection;
|
||||
/** Return metadata about resources being requested in the viewer */
|
||||
|
||||
@@ -1821,7 +1821,7 @@ export type Project = {
|
||||
team: Array<ProjectCollaborator>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
/** Retrieve a specific project version by its ID */
|
||||
version?: Maybe<Version>;
|
||||
version: Version;
|
||||
/** Returns a flat list of all project versions */
|
||||
versions: VersionCollection;
|
||||
/** Return metadata about resources being requested in the viewer */
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
<template>
|
||||
<ArrowPathIcon :class="iconClasses" />
|
||||
<svg
|
||||
class="spinner"
|
||||
:class="iconClasses"
|
||||
width="32px"
|
||||
height="40px"
|
||||
viewBox="0 0 66 66"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
class="path"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="6"
|
||||
stroke-linecap="round"
|
||||
cx="33"
|
||||
cy="33"
|
||||
r="30"
|
||||
></circle>
|
||||
</svg>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ArrowPathIcon } from '@heroicons/vue/24/solid'
|
||||
import { computed } from 'vue'
|
||||
|
||||
type Size = 'base' | 'sm' | 'lg'
|
||||
@@ -13,21 +30,57 @@ const props = withDefaults(defineProps<{ loading?: boolean; size?: Size }>(), {
|
||||
})
|
||||
|
||||
const iconClasses = computed(() => {
|
||||
const classParts: string[] = ['text-primary transition-all animate-spin']
|
||||
const classParts: string[] = ['']
|
||||
classParts.push(props.loading ? 'opacity-100' : 'opacity-0')
|
||||
|
||||
switch (props.size) {
|
||||
case 'base':
|
||||
classParts.push('h-8 w-8')
|
||||
break
|
||||
case 'sm':
|
||||
classParts.push('h-5 w-5')
|
||||
break
|
||||
case 'sm':
|
||||
classParts.push('h-4 w-4')
|
||||
break
|
||||
case 'lg':
|
||||
classParts.push('h-12 w-12')
|
||||
classParts.push('h-8 w-8')
|
||||
break
|
||||
}
|
||||
|
||||
return classParts.join(' ')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes rotator {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 187;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 46.75;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 187;
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: rotator 1.4s linear infinite;
|
||||
}
|
||||
|
||||
.path {
|
||||
stroke-dasharray: 187;
|
||||
stroke-dashoffset: 0;
|
||||
transform-origin: center;
|
||||
animation: dash 1.4s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,8 +25,8 @@ import { isObjectLike } from 'lodash'
|
||||
import type { PropAnyComponent } from '~~/src/helpers/common/components'
|
||||
import { computed, resolveDynamicComponent } from 'vue'
|
||||
import type { Nullable } from '@speckle/shared'
|
||||
import { ArrowPathIcon } from '@heroicons/vue/24/solid'
|
||||
import type { FormButtonStyle, FormButtonSize } from '~~/src/helpers/form/button'
|
||||
import { CommonLoadingIcon } from '~~/src/lib'
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
@@ -122,7 +122,9 @@ const buttonType = computed(() => {
|
||||
})
|
||||
|
||||
const isDisabled = computed(() => props.disabled || props.loading)
|
||||
const finalLeftIcon = computed(() => (props.loading ? ArrowPathIcon : props.iconLeft))
|
||||
const finalLeftIcon = computed(() =>
|
||||
props.loading ? CommonLoadingIcon : props.iconLeft
|
||||
)
|
||||
|
||||
const bgAndBorderClasses = computed(() => {
|
||||
const classParts: string[] = []
|
||||
@@ -266,10 +268,6 @@ const buttonClasses = computed(() => {
|
||||
const iconClasses = computed(() => {
|
||||
const classParts: string[] = ['shrink-0']
|
||||
|
||||
if (props.loading) {
|
||||
classParts.push('animate-spin')
|
||||
}
|
||||
|
||||
switch (props.size) {
|
||||
case 'sm':
|
||||
classParts.push('h-5 w-5 p-0.5')
|
||||
|
||||
@@ -154,6 +154,14 @@ export const LabelLeft = mergeStories(Default, {
|
||||
}
|
||||
})
|
||||
|
||||
export const Loading = mergeStories(Default, {
|
||||
args: {
|
||||
name: generateRandomName('loading'),
|
||||
label: 'With loading spinner',
|
||||
loading: true
|
||||
}
|
||||
})
|
||||
|
||||
export const WithCustomRightSlot = mergeStories(Default, {
|
||||
render: (args) => ({
|
||||
components: { FormTextInput, FormButton },
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute top-0 h-full right-0 flex items-center pr-2 text-foreground-3"
|
||||
>
|
||||
<CommonLoadingIcon />
|
||||
</div>
|
||||
|
||||
<input
|
||||
:id="name"
|
||||
ref="inputElement"
|
||||
@@ -115,6 +122,7 @@ import { useTextInputCore } from '~~/src/composables/form/textInput'
|
||||
import type { PropAnyComponent } from '~~/src/helpers/common/components'
|
||||
import type { InputColor } from '~~/src/composables/form/textInput'
|
||||
import type { LabelPosition } from '~~/src/composables/form/input'
|
||||
import { CommonLoadingIcon } from '~~/src/lib'
|
||||
|
||||
type InputType = 'text' | 'email' | 'password' | 'url' | 'search' | 'number' | string
|
||||
type InputSize = 'sm' | 'base' | 'lg' | 'xl'
|
||||
@@ -256,10 +264,18 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hideErrorMessage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
customErrorMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
wrapperClasses: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
|
||||
@@ -26,6 +26,7 @@ export function useTextInputCore<V extends string | string[] = string>(params: {
|
||||
autoFocus?: boolean
|
||||
showClear?: boolean
|
||||
useLabelInErrors?: boolean
|
||||
customErrorMessage?: string
|
||||
hideErrorMessage?: boolean
|
||||
color?: InputColor
|
||||
labelPosition?: LabelPosition
|
||||
@@ -41,11 +42,15 @@ export function useTextInputCore<V extends string | string[] = string>(params: {
|
||||
}) {
|
||||
const { props, inputEl, emit, options } = params
|
||||
|
||||
const { value, errorMessage: error } = useField<V>(props.name, props.rules, {
|
||||
validateOnMount: unref(props.validateOnMount),
|
||||
validateOnValueUpdate: unref(props.validateOnValueUpdate),
|
||||
initialValue: unref(props.modelValue) || undefined
|
||||
})
|
||||
const { value, errorMessage: veeErrorMessage } = useField<V>(
|
||||
props.name,
|
||||
props.rules,
|
||||
{
|
||||
validateOnMount: unref(props.validateOnMount),
|
||||
validateOnValueUpdate: unref(props.validateOnValueUpdate),
|
||||
initialValue: unref(props.modelValue) || undefined
|
||||
}
|
||||
)
|
||||
|
||||
const labelClasses = computed(() => {
|
||||
const classParts = [
|
||||
@@ -76,7 +81,7 @@ export function useTextInputCore<V extends string | string[] = string>(params: {
|
||||
coreInputClasses.value
|
||||
]
|
||||
|
||||
if (error.value) {
|
||||
if (hasError.value) {
|
||||
classParts.push('!border-danger')
|
||||
} else {
|
||||
classParts.push('border-0 focus:ring-2 focus:ring-outline-2')
|
||||
@@ -99,12 +104,19 @@ export function useTextInputCore<V extends string | string[] = string>(params: {
|
||||
const internalHelpTipId = ref(nanoid())
|
||||
|
||||
const title = computed(() => unref(props.label) || unref(props.name))
|
||||
|
||||
const errorMessage = computed(() => {
|
||||
const base = error.value
|
||||
if (unref(props.customErrorMessage)) {
|
||||
return unref(props.customErrorMessage)
|
||||
}
|
||||
|
||||
const base = veeErrorMessage.value
|
||||
if (!base || !unref(props.useLabelInErrors)) return base
|
||||
return base.replace('Value', title.value)
|
||||
})
|
||||
|
||||
const hasError = computed(() => !!errorMessage.value)
|
||||
|
||||
const hideHelpTip = computed(
|
||||
() => errorMessage.value && unref(props.hideErrorMessage)
|
||||
)
|
||||
@@ -115,7 +127,7 @@ export function useTextInputCore<V extends string | string[] = string>(params: {
|
||||
)
|
||||
const helpTipClasses = computed((): string => {
|
||||
const classParts = ['text-body-2xs break-words']
|
||||
classParts.push(error.value ? 'text-danger' : 'text-foreground-2')
|
||||
classParts.push(hasError.value ? 'text-danger' : 'text-foreground-2')
|
||||
return classParts.join(' ')
|
||||
})
|
||||
const shouldShowClear = computed(() => {
|
||||
@@ -154,7 +166,8 @@ export function useTextInputCore<V extends string | string[] = string>(params: {
|
||||
clear,
|
||||
focus,
|
||||
labelClasses,
|
||||
shouldShowClear
|
||||
shouldShowClear,
|
||||
hasError
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1357,11 +1357,11 @@ export default class Sandbox {
|
||||
url.includes('latest') ? 'AuthTokenLatest' : 'AuthToken'
|
||||
) as string
|
||||
const objUrls = await UrlHelper.getResourceUrls(url, authToken)
|
||||
for (const url of objUrls) {
|
||||
for (const objUrl of objUrls) {
|
||||
console.log(`Loading ${url}`)
|
||||
const loader = new SpeckleLoader(
|
||||
this.viewer.getWorldTree(),
|
||||
url,
|
||||
objUrl,
|
||||
authToken,
|
||||
true,
|
||||
undefined
|
||||
@@ -1377,7 +1377,7 @@ export default class Sandbox {
|
||||
console.error(`Loader warning: ${arg.message}`)
|
||||
})
|
||||
|
||||
await this.viewer.loadObject(loader, true)
|
||||
void this.viewer.loadObject(loader, true)
|
||||
}
|
||||
localStorage.setItem('last-load-url', url)
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ const getStream = () => {
|
||||
// prettier-ignore
|
||||
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D'
|
||||
// Revit sample house (good for bim-like stuff with many display meshes)
|
||||
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
|
||||
'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
|
||||
// 'https://latest.speckle.systems/streams/c1faab5c62/commits/ab1a1ab2b6'
|
||||
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
|
||||
// 'https://latest.speckle.systems/streams/58b5648c4d/commits/60371ecb2d'
|
||||
@@ -424,7 +424,8 @@ const getStream = () => {
|
||||
|
||||
// 'https://app.speckle.systems/projects/00a5c443d6/models/de56edf901'
|
||||
|
||||
'https://app.speckle.systems/projects/6cf358a40e/models/e01ffbc891@f1ddc19011'
|
||||
// 'https://app.speckle.systems/projects/6cf358a40e/models/e01ffbc891@f1ddc19011'
|
||||
// 'https://latest.speckle.systems/projects/2c5c4cd493/models/cb20c2e87b02003d4b2ddf4df96912b9,8a2c9a45093fecb42273607d0c936d66'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export default class Batcher {
|
||||
await pause.wait(50)
|
||||
}
|
||||
|
||||
let instancedNodes = worldTree.findId(g)
|
||||
let instancedNodes = worldTree.findId(g, renderTree.subtreeId)
|
||||
if (!instancedNodes) {
|
||||
continue
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export default class Batcher {
|
||||
}
|
||||
for (const v in instancedBatches) {
|
||||
for (let k = 0; k < instancedBatches[v].length; k++) {
|
||||
const nodes = worldTree.findId(instancedBatches[v][k])
|
||||
const nodes = worldTree.findId(instancedBatches[v][k], renderTree.subtreeId)
|
||||
if (!nodes) continue
|
||||
/** Make sure entire instance set is instanced */
|
||||
let instanced = true
|
||||
|
||||
@@ -15,6 +15,10 @@ export class RenderTree {
|
||||
return this.root.model.id
|
||||
}
|
||||
|
||||
public get subtreeId(): number {
|
||||
return this.root.model.subtreeId
|
||||
}
|
||||
|
||||
public constructor(tree: WorldTree, subtreeRoot: TreeNode) {
|
||||
this.tree = tree
|
||||
this.root = subtreeRoot
|
||||
@@ -192,8 +196,11 @@ export class RenderTree {
|
||||
})
|
||||
}
|
||||
|
||||
public getRenderViewsForNodeId(id: string): NodeRenderView[] | null {
|
||||
const nodes = this.tree.findId(id)
|
||||
public getRenderViewsForNodeId(
|
||||
id: string,
|
||||
subtreeId?: number
|
||||
): NodeRenderView[] | null {
|
||||
const nodes = this.tree.findId(id, subtreeId)
|
||||
if (!nodes) {
|
||||
Logger.warn(`Id ${id} does not exist`)
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user