Compare commits

...

17 Commits

Author SHA1 Message Date
Björn Steinhagen ac09115aed chore: probs shouldnt be removing commented out stuff 2026-04-15 15:31:50 +02:00
Björn Steinhagen 22bca347f1 chore: resolve ts problems 2026-04-15 14:40:17 +02:00
Björn Steinhagen 35ee546fc5 chore: puts eslint-disable back 2026-04-15 14:36:18 +02:00
Björn Steinhagen 2d086a4ab6 fix: guard against undefined account in Sender and Receiver model card setup 2026-04-15 14:27:48 +02:00
Björn Steinhagen 9c73769ec6 refactor: simplify inaccessible reason to two cases based on server query behaviour 2026-04-15 13:50:58 +02:00
Björn Steinhagen 59b0c690f0 fix: improve inaccessible project error messages for actionability 2026-04-15 11:35:35 +02:00
Björn Steinhagen 6eaf5e2135 chore: error message and comments 2026-04-15 11:00:19 +02:00
Björn Steinhagen 43fe995bd8 fix: clarify inaccessible project error messages 2026-04-15 10:35:16 +02:00
Björn Steinhagen 941b3442fa feat: show contextual error messages on inaccessible project cards 2026-04-15 10:30:09 +02:00
Björn Steinhagen 4fabaa4fde fix: suppress inaccessible project UI on SSO expiry 2026-04-15 10:17:47 +02:00
Björn Steinhagen 30303e6783 fix: recover model card state reactively on account add or validity change 2026-04-15 10:13:16 +02:00
Björn Steinhagen ba08c93c51 fix: suppress inaccessible project UI on SSO expiry 2026-04-15 09:34:42 +02:00
Björn Steinhagen ed9f2ba40d chore: add failure logging for inaccessible projects 2026-04-15 09:28:49 +02:00
Björn Steinhagen ded9f7d041 fix: surface inaccessible project UI and prevent false positives on shared files 2026-04-15 09:24:48 +02:00
Björn Steinhagen 3a2475b375 chore: adds temporary logging 2026-04-14 22:07:58 +02:00
Björn Steinhagen baaf73d438 fix: removeProjectModels not updating local store after removing from host app 2026-04-14 17:17:42 +02:00
Björn Steinhagen b4794f1fd3 fix: account existence check falls back to server URL, guard watch against loading state 2026-04-14 17:03:54 +02:00
4 changed files with 116 additions and 30 deletions
+99 -21
View File
@@ -83,22 +83,35 @@
@dismiss="askDismissProjectQuestionDialog = true"
>
<template #title>
Whoops - project
<code>{{ project.projectId }}</code>
is inaccessible.
<span v-if="inaccessibleReason === 'no-account'">
<!-- no local account matches this project's server query was never attempted -->
No account found for
<code>{{ project.serverUrl }}</code>
</span>
<span v-else>
<!-- project query threw server will throw for any reason the account can't see
the project (deleted, private, auth failure, stale token). model card level
permission errors e.g. role changes surface separately on the card itself -->
Project
<code>{{ project.projectId }}</code>
is not accessible on
<code>{{ project.serverUrl }}</code>
</span>
</template>
</CommonAlert>
<CommonDialog v-model:open="askDismissProjectQuestionDialog" fullscreen="none">
<template #header>Remove Project</template>
<div class="text-xs mb-4">Do you want to remove the project from this file?</div>
<div class="flex justify-between center py-2 space-x-3">
<FormButton size="sm" full-width @click="removeProjectModels">Yes</FormButton>
<FormButton size="sm" full-width @click="removeProjectModels">
Remove
</FormButton>
<FormButton
size="sm"
full-width
@click="askDismissProjectQuestionDialog = false"
>
Hide error
Cancel
</FormButton>
</div>
</CommonDialog>
@@ -131,6 +144,7 @@ const showModels = ref(true)
const askDismissProjectQuestionDialog = ref(false)
const writeAccessRequested = ref(false)
const projectIsAccesible = ref<boolean | undefined>(undefined)
const inaccessibleReason = ref<'no-account' | 'error' | undefined>(undefined)
const projectAccount = computed(() =>
accountStore.accountWithFallback(props.project.accountId, props.project.serverUrl)
@@ -143,13 +157,22 @@ const projectNavigatorTippy = computed(() =>
: 'Open project in browser'
)
const clientId = projectAccount.value.accountInfo.id
const normalizeUrl = (url: string) => url.replace(/\/$/, '').toLowerCase()
const accountExists = accountStore.isAccountExistsById(props.project.accountId)
// match by account ID first, then fall back to server URL
// normalized to avoid trailing-slash / casing mismatches (can that even be a thing??)
const accountExists = computed(() => {
const byId = accountStore.isAccountExistsById(props.project.accountId)
const byServer = accountStore.accounts.some(
(acc) =>
normalizeUrl(acc.accountInfo.serverInfo.url) ===
normalizeUrl(props.project.serverUrl)
)
return byId || byServer
})
if (!accountExists) {
projectIsAccesible.value = false
}
// reactive so it re-derives if accounts change after mount (e.g. user adds the missing account)
const clientId = computed(() => projectAccount.value.accountInfo.id)
const {
result: projectDetailsResult,
@@ -159,13 +182,26 @@ const {
projectDetailsQuery,
() => ({ projectId: props.project.projectId }),
() => ({
clientId,
clientId: clientId.value,
debounce: 500,
fetchPolicy: 'network-only',
enabled: accountExists
enabled: accountExists.value
})
)
// re-run the query when the resolved account changes (account added) or
// when accountExists flips back to true (account re-added after removal)
watch([clientId, accountExists], ([, exists], [, prevExists]) => {
if (exists) {
if (!prevExists) {
// accountExists just recovered — reset accessible state so we don't
// show stale inaccessible UI while the query is in flight
projectIsAccesible.value = undefined
}
void refetchProjectDetails()
}
})
const removeProjectModels = async () => {
await hostAppStore.removeProjectModels(props.project.projectId)
askDismissProjectQuestionDialog.value = false
@@ -173,12 +209,54 @@ const removeProjectModels = async () => {
const projectDetails = computed(() => projectDetailsResult.value?.project)
watch(projectDetails, (newValue) => {
projectIsAccesible.value = newValue !== undefined
watch(
[projectDetails, accountExists],
([details, exists]) => {
if (!exists) {
// account not present on this machine at all
console.warn('[ProjectModelGroup] inaccessible: no matching account', {
projectId: props.project.projectId,
storedAccountId: props.project.accountId,
storedServerUrl: props.project.serverUrl,
localAccounts: accountStore.accounts.map((a) => ({
id: a.accountInfo.id,
serverUrl: a.accountInfo.serverInfo.url
}))
})
inaccessibleReason.value = 'no-account'
projectIsAccesible.value = false
} else if (details !== undefined) {
// query returned real data — project is accessible
inaccessibleReason.value = undefined
projectIsAccesible.value = true
}
// undefined means the query is still loading; don't update state yet
},
{ immediate: true }
)
onProjectDetailsError((error) => {
// the project query throws for any reason the account can't see the project —
// deleted, private, auth failure, network error. all cases show the same message.
console.warn('[ProjectModelGroup] inaccessible: project query errored', {
projectId: props.project.projectId,
accountId: props.project.accountId,
error: error.message
})
inaccessibleReason.value = 'error'
projectIsAccesible.value = false
})
onProjectDetailsError(() => {
projectIsAccesible.value = false
// when the account's validity changes (e.g. SSO session deleted externally), refetch
// so the query hits the server and picks up the new auth state
const accountIsValid = computed(
() => accountStore.accounts.find((a) => a.accountInfo.id === clientId.value)?.isValid
)
watch(accountIsValid, (isValid, wasValid) => {
if (wasValid !== undefined && isValid !== wasValid) {
void refetchProjectDetails()
}
})
const canLoad = computed(() => !!projectDetails.value?.permissions.canLoad.authorized)
@@ -213,13 +291,13 @@ const isWorkspaceReadOnly = computed(() => {
const { onResult: userProjectsUpdated } = useSubscription(
userProjectsUpdatedSubscription,
() => ({}),
() => ({ clientId, enabled: accountExists })
() => ({ clientId: clientId.value, enabled: accountExists.value })
)
const { onResult: projectUpdated } = useSubscription(
projectUpdatedSubscription,
() => ({ projectId: props.project.projectId }),
() => ({ clientId, enabled: accountExists })
() => ({ clientId: clientId.value, enabled: accountExists.value })
)
// to catch changes on visibility of project
@@ -238,14 +316,14 @@ userProjectsUpdated((res) => {
})
const projectUrl = computed(() => {
const acc = accountStore.accounts.find((acc) => acc.accountInfo.id === clientId)
const acc = accountStore.accounts.find((acc) => acc.accountInfo.id === clientId.value)
return `${acc?.accountInfo.serverInfo.url as string}/projects/${
props.project.projectId
}`
})
const workspaceUrl = computed(() => {
const acc = accountStore.accounts.find((acc) => acc.accountInfo.id === clientId)
const acc = accountStore.accounts.find((acc) => acc.accountInfo.id === clientId.value)
return `${acc?.accountInfo.serverInfo.url as string}/workspaces/${
projectDetails.value?.workspace?.slug
}`
@@ -255,7 +333,7 @@ const workspaceUrl = computed(() => {
const { onResult } = useSubscription(
versionCreatedSubscription,
() => ({ projectId: props.project.projectId }),
() => ({ clientId, enabled: accountExists })
() => ({ clientId: clientId.value, enabled: accountExists.value })
)
onResult((res) => {
+4 -2
View File
@@ -40,7 +40,6 @@
{{ createdAgo }}
</span>
</div> -->
<CommonDialog
v-model:open="openVersionsDialog"
fullscreen="none"
@@ -271,6 +270,8 @@ const errorNotification = computed(() => {
return notification
})
const clientId = computed(() => projectAccount.value?.accountInfo.id)
const { result: versionDetailsResult, refetch } = useQuery(
versionDetailsQuery,
() => ({
@@ -279,7 +280,8 @@ const { result: versionDetailsResult, refetch } = useQuery(
versionId: props.modelCard.selectedVersionId
}),
() => ({
clientId: projectAccount.value.accountInfo.id
clientId: clientId.value,
enabled: !!clientId.value
})
)
+10 -7
View File
@@ -133,7 +133,8 @@ import {
useMutation,
useSubscription
} from '@vue/apollo-composable'
import { useAccountStore, type DUIAccount } from '~/store/accounts'
import { useAccountStore } from '~/store/accounts'
import type { ApolloClient } from '@apollo/client/core'
import { setVersionMessageMutation } from '~/lib/graphql/mutationsAndQueries'
import { workspacePlanUsageUpdatedSubscription } from '~/lib/workspaces/graphql/subscriptions'
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
@@ -152,10 +153,10 @@ const props = defineProps<{
canEdit: boolean
}>()
const account = accountStore.accounts.find(
(acc) => acc.accountInfo.id === props.project.accountId
) as DUIAccount
const clientId = account.accountInfo.id
const projectAccount = computed(() =>
accountStore.accountWithFallback(props.project.accountId, props.project.serverUrl)
)
const clientId = computed(() => projectAccount.value?.accountInfo.id)
const openFilterDialog = ref(false)
app.$baseBinding?.on('documentChanged', () => {
@@ -189,7 +190,7 @@ const { onResult: onWorkspacePlanUsageUpdated } = useSubscription(
workspaceId: props.modelCard.workspaceId as string
}
}),
() => ({ clientId })
() => ({ clientId: clientId.value })
)
onWorkspacePlanUsageUpdated(() => {
@@ -253,7 +254,9 @@ const setVersionMessage = async (message: string) => {
})
isUpdatingVersionMessage.value = true
const { mutate } = provideApolloClient(account.client)(() =>
const client = projectAccount.value?.client as ApolloClient<unknown> | undefined
if (!client) return
const { mutate } = provideApolloClient(client)(() =>
useMutation(setVersionMessageMutation)
)
+3
View File
@@ -218,6 +218,9 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
)
if (modelsToRemove.length !== 0) {
await app.$baseBinding.removeModels(modelsToRemove)
documentModelStore.value.models = documentModelStore.value.models.filter(
(item) => item.projectId !== projectId
)
}
}