Compare commits

...

4 Commits

Author SHA1 Message Date
Kristaps Fabians Geikins 574063e9a9 fixes 2025-05-13 16:59:51 +03:00
Kristaps Fabians Geikins 168824c58b add caching 2025-05-13 16:27:30 +03:00
Kristaps Fabians Geikins 245d414e6d fixx 2025-05-13 16:25:56 +03:00
Kristaps Fabians Geikins a895edb069 introduce CI checks 2025-05-13 16:24:05 +03:00
23 changed files with 1412 additions and 1589 deletions
-1
View File
File diff suppressed because one or more lines are too long
+44
View File
@@ -0,0 +1,44 @@
name: Linting
on:
pull_request:
branches:
- main
jobs:
lint-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22.14.0"
- name: Enable Corepack and Install Correct Yarn Version
run: |
corepack enable
corepack prepare yarn@$(jq -r .packageManager package.json | cut -d'@' -f2) --activate
yarn --version
- name: Cache node_modules
uses: actions/cache@v4
with:
path: |
**/node_modules
.yarn/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Dependencies
run: yarn install --immutable
- name: Run Linter
run: yarn lint
- name: Run generate
run: yarn generate
+47
View File
@@ -0,0 +1,47 @@
node_modules
build
dist
dist2
dist-*
coverage
.nyc_output
packages/server/reports*
packages/preview-service/public/**/*
packages/objectloader/examples/browser/objectloader.web.js
packages/viewer/example/speckleviewer.web.js
.output
.nuxt
**/nuxt-modules/**/templates/*.js
packages/frontend-2/lib/common/generated/**/*
packages/dui3/lib/common/generated/**/*
packages/server/introspected-schema.graphql
package-lock.json
yarn.lock
.yarn
# Profiler output
events.json
# Prettier doesn't understand the syntax inside the Yaml files, because of the brackets
utils/helm/speckle-server/templates
# Optional eslint cache
.eslintcache
.venv
venv
.*.{ts,js,vue,tsx,jsx}
**/generated/**/*
**/generated/graphql.ts
storybook-static
.tshy
.tshy-build
packages/fileimport-service/ifc-dotnet/
packages/fileimport-service/stl/
packages/fileimport-service/obj/
packages/shared/html
+11
View File
@@ -0,0 +1,11 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"endOfLine": "auto",
"bracketSpacing": true,
"vueIndentScriptAndStyle": false,
"htmlWhitespaceSensitivity": "ignore",
"printWidth": 88,
"singleQuote": true
}
-1
View File
@@ -52,4 +52,3 @@ onMounted(() => {
})
})
</script>
<style></style>
+1 -1
View File
@@ -60,7 +60,7 @@
<script setup lang="ts">
import type { DUIAccount } from '~~/store/accounts'
import { TrashIcon } from '@heroicons/vue/24/outline'
import { type BaseBridge } from '~/lib/bridge/base'
import type { BaseBridge } from '~/lib/bridge/base'
const { $accountBinding } = useNuxtApp()
+1
View File
@@ -84,6 +84,7 @@ import { useAccountStore } from '~/store/accounts'
import type { ApolloError } from '@apollo/client/errors'
import { formatVersionParams } from '~/lib/common/helpers/jsonSchema'
import { useJsonFormsChangeHandler } from '~/lib/core/composables/jsonSchema'
import {isArray} from 'lodash-es'
const props = defineProps<{
projectId: string
+22 -19
View File
@@ -7,37 +7,40 @@
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import type { IDirectSelectionSendFilter, ISendFilter } from 'lib/models/card/send'
import { useHostAppStore } from '~~/store/hostApp'
import { useSelectionStore } from '~~/store/selection'
import { storeToRefs } from "pinia";
import type {
IDirectSelectionSendFilter,
ISendFilter,
} from "~/lib/models/card/send";
import { useHostAppStore } from "~~/store/hostApp";
import { useSelectionStore } from "~~/store/selection";
const emit = defineEmits<{
(e: 'update:filter', filter: ISendFilter): void
}>()
(e: "update:filter", filter: ISendFilter): void;
}>();
const store = useHostAppStore()
const { selectionFilter } = storeToRefs(store)
const store = useHostAppStore();
const { selectionFilter } = storeToRefs(store);
const selectionStore = useSelectionStore()
const { selectionInfo } = storeToRefs(selectionStore)
const selectionStore = useSelectionStore();
const { selectionInfo } = storeToRefs(selectionStore);
defineProps<{
filter: IDirectSelectionSendFilter
}>()
filter: IDirectSelectionSendFilter;
}>();
watch(
selectionInfo,
(newValue) => {
const filter = { ...selectionFilter.value } as IDirectSelectionSendFilter
filter.selectedObjectIds = newValue.selectedObjectIds
filter.summary = newValue.summary as string
emit('update:filter', filter)
const filter = { ...selectionFilter.value } as IDirectSelectionSendFilter;
filter.selectedObjectIds = newValue.selectedObjectIds;
filter.summary = newValue.summary as string;
emit("update:filter", filter);
},
{ deep: true, immediate: true }
)
);
onMounted(() => {
selectionStore.refreshSelectionFromHostApp()
})
selectionStore.refreshSelectionFromHostApp();
});
</script>
+40 -34
View File
@@ -9,21 +9,25 @@
<CheckCircleIcon class="w-4 stroke-green-500 text-green-500" />
</div>
<div v-else-if="reportItem.status === 3">
<ExclamationTriangleIcon class="w-4 text-warning"></ExclamationTriangleIcon>
<ExclamationTriangleIcon
class="w-4 text-warning"
></ExclamationTriangleIcon>
</div>
<div v-else>
<ExclamationCircleIcon class="w-4 text-danger"></ExclamationCircleIcon>
<ExclamationCircleIcon
class="w-4 text-danger"
></ExclamationCircleIcon>
</div>
</div>
<div class="text-xs transition truncate">
<span v-if="reportItem.status === 1">
{{ reportItem.sourceType?.split('.').reverse()[0] }} >
{{ reportItem.sourceType?.split(".").reverse()[0] }} >
</span>
<span>
{{
reportItem.resultType
? reportItem.resultType?.split('.').reverse()[0]
? reportItem.resultType?.split(".").reverse()[0]
: reportItem.error?.message
}}
</span>
@@ -71,65 +75,67 @@ import {
CheckCircleIcon,
ChevronUpIcon,
ChevronDownIcon,
ArrowTopRightOnSquareIcon
} from '@heroicons/vue/24/solid'
import type { ConversionResult } from '~/lib/conversions/conversionResult'
import { useAccountStore } from '~/store/accounts'
import type { IModelCard } from 'lib/models/card'
import { useHostAppStore } from '~/store/hostApp'
ArrowTopRightOnSquareIcon,
} from "@heroicons/vue/24/solid";
import type { ConversionResult } from "~/lib/conversions/conversionResult";
import { useAccountStore } from "~/store/accounts";
import type { IModelCard } from "~/lib/models/card";
import { useHostAppStore } from "~/store/hostApp";
const app = useNuxtApp()
const hostAppStore = useHostAppStore()
const accStore = useAccountStore()
const app = useNuxtApp();
const hostAppStore = useHostAppStore();
const accStore = useAccountStore();
const showDetails = ref<boolean>(false)
const showDetails = ref<boolean>(false);
const props = defineProps<{
reportItem: ConversionResult
}>()
reportItem: ConversionResult;
}>();
const cardBase = inject('cardBase') as IModelCard
const cardBase = inject("cardBase") as IModelCard;
const isSender = computed(() =>
hostAppStore.models
.find((m) => m.modelCardId === cardBase.modelCardId)
?.typeDiscriminator.toLowerCase()
.includes('sender')
)
.includes("sender")
);
const acc = accStore.accounts.find((acc) => acc.accountInfo.id === cardBase.accountId)
const acc = accStore.accounts.find(
(acc) => acc.accountInfo.id === cardBase.accountId
);
const details = computed(() =>
props.reportItem.error
? props.reportItem.error.stackTrace
: `${props.reportItem.sourceType} > ${props.reportItem.resultType}`
)
);
const openObjectOnWeb = () => {
// This is a POC implementation. Later we will highlight object(s) within the model. Currently it is done by 'Isolate' filter on viewer but there is no direct URL to achieve this.
const url = `${acc?.accountInfo.serverInfo.url}/projects/${cardBase?.projectId}/models/${props.reportItem.sourceId}`
app.$openUrl(url)
}
const url = `${acc?.accountInfo.serverInfo.url}/projects/${cardBase?.projectId}/models/${props.reportItem.sourceId}`;
app.$openUrl(url);
};
const highlightObject = () => {
// sender reports highlight in source app
if (cardBase.typeDiscriminator.toLowerCase().includes('send')) {
app.$baseBinding.highlightObjects([props.reportItem.sourceId])
return
if (cardBase.typeDiscriminator.toLowerCase().includes("send")) {
app.$baseBinding.highlightObjects([props.reportItem.sourceId]);
return;
}
// receive reports that are ok highliht in source app
if (props.reportItem.status === 1 && props.reportItem.resultId) {
app.$baseBinding.highlightObjects([props.reportItem.resultId])
return
app.$baseBinding.highlightObjects([props.reportItem.resultId]);
return;
}
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text)
}
navigator.clipboard.writeText(text);
};
const toggleDetails = () => {
showDetails.value = !showDetails.value
}
showDetails.value = !showDetails.value;
};
</script>
+144 -108
View File
@@ -10,10 +10,14 @@
<FormButton
full-width
class="flex items-center"
@click="$openUrl('https://app.speckle.systems/workspaces/actions/create')"
@click="
$openUrl(
'https://app.speckle.systems/workspaces/actions/create'
)
"
>
<div class="min-w-0 truncate flex-grow">
<span>{{ 'Create a workspace' }}</span>
<span>{{ "Create a workspace" }}</span>
</div>
<ArrowTopRightOnSquareIcon class="w-4" />
</FormButton>
@@ -73,7 +77,9 @@
/>
<div class="flex justify-between items-center space-x-2">
<ProjectCreateWorkspaceDialog
v-if="selectedWorkspace && selectedWorkspace.id !== 'personalProject'"
v-if="
selectedWorkspace && selectedWorkspace.id !== 'personalProject'
"
:workspace="selectedWorkspace"
@project:created="(result : ProjectListProjectItemFragment) => handleProjectCreated(result)"
>
@@ -126,143 +132,169 @@
color="outline"
@click="loadMore"
>
{{ hasReachedEnd ? 'No more projects found' : 'Load older projects' }}
{{ hasReachedEnd ? "No more projects found" : "Load older projects" }}
</FormButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ChevronDownIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/outline'
import { storeToRefs } from 'pinia'
import { PlusIcon } from '@heroicons/vue/20/solid'
import type { DUIAccount } from '~/store/accounts'
import { useAccountStore } from '~/store/accounts'
import {
ChevronDownIcon,
ArrowTopRightOnSquareIcon,
} from "@heroicons/vue/24/outline";
import { storeToRefs } from "pinia";
import { PlusIcon } from "@heroicons/vue/20/solid";
import type { DUIAccount } from "~/store/accounts";
import { useAccountStore } from "~/store/accounts";
import {
activeWorkspaceQuery,
projectsListQuery,
serverInfoQuery,
setActiveWorkspaceMutation,
workspacesListQuery
} from '~/lib/graphql/mutationsAndQueries'
import { useMutation, provideApolloClient, useQuery } from '@vue/apollo-composable'
workspacesListQuery,
} from "~/lib/graphql/mutationsAndQueries";
import {
useMutation,
provideApolloClient,
useQuery,
} from "@vue/apollo-composable";
import type {
ProjectListProjectItemFragment,
WorkspaceListWorkspaceItemFragment
} from 'lib/common/generated/gql/graphql'
import { useMixpanel } from '~/lib/core/composables/mixpanel'
import { useConfigStore } from '~/store/config'
WorkspaceListWorkspaceItemFragment,
} from "~/lib/common/generated/gql/graphql";
import { useMixpanel } from "~/lib/core/composables/mixpanel";
import { useConfigStore } from "~/store/config";
const { trackEvent } = useMixpanel()
const { $openUrl } = useNuxtApp()
const { trackEvent } = useMixpanel();
const { $openUrl } = useNuxtApp();
const emit = defineEmits<{
(
e: 'next',
e: "next",
accountId: string,
project: ProjectListProjectItemFragment,
workspace?: WorkspaceListWorkspaceItemFragment // NOTE: this nullabilities will disappear whenever we are workspace only
): void
(e: 'search-text-update', text: string | undefined): void
}>()
): void;
(e: "search-text-update", text: string | undefined): void;
}>();
const props = withDefaults(
defineProps<{
isSender: boolean
showNewProject?: boolean
isSender: boolean;
showNewProject?: boolean;
/**
* For the send wizard - not allowing selecting projects we can't write to.
*/
disableNoWriteAccessProjects?: boolean
disableNoWriteAccessProjects?: boolean;
}>(),
{
showNewProject: true,
disableNoWriteAccessProjects: false
disableNoWriteAccessProjects: false,
}
)
);
const searchText = ref<string>()
const newProjectName = ref<string>()
const accountStore = useAccountStore()
const configStore = useConfigStore()
const { activeAccount } = storeToRefs(accountStore)
const searchText = ref<string>();
const newProjectName = ref<string>();
const accountStore = useAccountStore();
const configStore = useConfigStore();
const { activeAccount } = storeToRefs(accountStore);
const accountId = computed(() => activeAccount.value.accountInfo.id)
const accountId = computed(() => activeAccount.value.accountInfo.id);
watch(searchText, () => {
newProjectName.value = searchText.value
emit('search-text-update', searchText.value)
})
newProjectName.value = searchText.value;
emit("search-text-update", searchText.value);
});
// TODO: this function is never triggered!! remove or evaluate
const selectAccount = (account: DUIAccount) => {
refetchServerInfo() // to be able to understand workspaces enabled or not
refetchActiveWorkspace()
refetchWorkspaces()
void trackEvent('DUI3 Action', { name: 'Account Select' }, account.accountInfo.id)
}
refetchServerInfo(); // to be able to understand workspaces enabled or not
refetchActiveWorkspace();
refetchWorkspaces();
void trackEvent(
"DUI3 Action",
{ name: "Account Select" },
account.accountInfo.id
);
};
const handleProjectCreated = (result: ProjectListProjectItemFragment) => {
refetch() // Sorts the list with newly created project otherwise it will put the project at the bottom.
emit('next', accountId.value, result)
}
refetch(); // Sorts the list with newly created project otherwise it will put the project at the bottom.
emit("next", accountId.value, result);
};
const { result: serverInfoResult, refetch: refetchServerInfo } = useQuery(
serverInfoQuery,
() => ({}),
() => ({ clientId: accountId.value, debounce: 500, fetchPolicy: 'network-only' })
)
() => ({
clientId: accountId.value,
debounce: 500,
fetchPolicy: "network-only",
})
);
const workspacesEnabled = computed(
() => serverInfoResult.value?.serverInfo.workspaces.workspacesEnabled
)
);
const { result: workspacesResult, refetch: refetchWorkspaces } = useQuery(
workspacesListQuery,
() => ({
limit: 100
limit: 100,
}),
() => ({ clientId: accountId.value, debounce: 500, fetchPolicy: 'network-only' })
)
() => ({
clientId: accountId.value,
debounce: 500,
fetchPolicy: "network-only",
})
);
const workspaces = computed(() => workspacesResult.value?.activeUser?.workspaces.items)
const workspaces = computed(
() => workspacesResult.value?.activeUser?.workspaces.items
);
const { result: activeWorkspaceResult, refetch: refetchActiveWorkspace } = useQuery(
activeWorkspaceQuery,
() => ({}),
() => ({ clientId: accountId.value, debounce: 500, fetchPolicy: 'network-only' })
)
const { result: activeWorkspaceResult, refetch: refetchActiveWorkspace } =
useQuery(
activeWorkspaceQuery,
() => ({}),
() => ({
clientId: accountId.value,
debounce: 500,
fetchPolicy: "network-only",
})
);
const activeWorkspace = computed(() => {
const userSelectedWorkspaceId = configStore.userSelectedWorkspaceId
const userSelectedWorkspaceId = configStore.userSelectedWorkspaceId;
if (userSelectedWorkspaceId) {
const previouslySelectedWorkspace = workspaces.value?.find(
(w) => w.id === userSelectedWorkspaceId
)
);
if (previouslySelectedWorkspace) {
return previouslySelectedWorkspace
return previouslySelectedWorkspace;
}
}
// fallback to activeWorkspace query result
return activeWorkspaceResult.value?.activeUser
?.activeWorkspace as WorkspaceListWorkspaceItemFragment
})
?.activeWorkspace as WorkspaceListWorkspaceItemFragment;
});
const selectedWorkspace = ref<WorkspaceListWorkspaceItemFragment | undefined>(
activeWorkspace.value
)
);
watch(
workspaces,
(newItems) => {
if (newItems && newItems.length > 0) {
selectedWorkspace.value = activeWorkspace.value ?? newItems[0]
selectedWorkspace.value = activeWorkspace.value ?? newItems[0];
} else {
selectedWorkspace.value = undefined
selectedWorkspace.value = undefined;
}
},
{ immediate: true }
)
);
const handleProjectCardClick = (project: ProjectListProjectItemFragment) => {
if (
@@ -270,108 +302,112 @@ const handleProjectCardClick = (project: ProjectListProjectItemFragment) => {
? project.permissions.canPublish.authorized
: project.permissions.canLoad.authorized
) {
emit('next', accountId.value, project, selectedWorkspace.value)
emit("next", accountId.value, project, selectedWorkspace.value);
}
}
};
const handleWorkspaceSelected = async (
newSelectedWorkspace: WorkspaceListWorkspaceItemFragment
) => {
selectedWorkspace.value = newSelectedWorkspace
selectedWorkspace.value = newSelectedWorkspace;
const account = computed(() => {
return accountStore.accounts.find(
(acc) => acc.accountInfo.id === accountId.value
) as DUIAccount
})
) as DUIAccount;
});
const { mutate } = provideApolloClient(account.value.client)(() =>
useMutation(setActiveWorkspaceMutation)
)
);
try {
await mutate({ slug: newSelectedWorkspace.slug })
await mutate({ slug: newSelectedWorkspace.slug });
} catch (error) {
// I dont believe we should throw toast for this, but good to be critical on console
console.error(error)
console.error(error);
}
configStore.setUserSelectedWorkspace(newSelectedWorkspace.id)
}
configStore.setUserSelectedWorkspace(newSelectedWorkspace.id);
};
// This is a hack for people who don't have a workspace and have personal projects only.
const timeoutWait = ref(false)
const timeoutWait = ref(false);
const filtersReady = computed(
() => selectedWorkspace.value !== undefined || timeoutWait.value
)
);
onMounted(() => {
setTimeout(() => {
timeoutWait.value = true
}, 1000)
})
timeoutWait.value = true;
}, 1000);
});
const {
result: projectsResult,
loading,
fetchMore,
refetch
refetch,
} = useQuery(
projectsListQuery,
() => ({
limit: 10, // stupid hack, increased it since we do manual filter to be able to see more project, see below TODO note, once we have `personalOnly` filter, decrease back to 10
filter: {
search: (searchText.value || '').trim() || null,
search: (searchText.value || "").trim() || null,
workspaceId:
selectedWorkspace.value?.id === 'personalProject'
selectedWorkspace.value?.id === "personalProject"
? null
: selectedWorkspace.value?.id,
includeImplicitAccess: true,
personalOnly: selectedWorkspace.value?.id === 'personalProject'
}
personalOnly: selectedWorkspace.value?.id === "personalProject",
},
}),
() => ({
enabled: filtersReady.value,
clientId: accountId.value,
debounce: 500,
fetchPolicy: 'network-only'
fetchPolicy: "network-only",
})
)
);
const projects = computed(() =>
selectedWorkspace.value?.id === 'personalProject' // TODO: we need to replace this logic with `personalOnly` filter when it is implemented into app.speckle.systems
selectedWorkspace.value?.id === "personalProject" // TODO: we need to replace this logic with `personalOnly` filter when it is implemented into app.speckle.systems
? projectsResult.value?.activeUser?.projects.items.filter(
(i) => i.workspaceId === null
)
: projectsResult.value?.activeUser?.projects.items
)
const hasReachedEnd = ref(false)
);
const hasReachedEnd = ref(false);
watch(searchText, () => {
hasReachedEnd.value = false
})
hasReachedEnd.value = false;
});
watch(projectsResult, (newVal) => {
if (
newVal &&
newVal.activeUser &&
newVal?.activeUser?.projects.items.length >= newVal?.activeUser?.projects.totalCount
newVal?.activeUser?.projects.items.length >=
newVal?.activeUser?.projects.totalCount
) {
hasReachedEnd.value = true
hasReachedEnd.value = true;
} else {
hasReachedEnd.value = false
hasReachedEnd.value = false;
}
})
});
const loadMore = () => {
fetchMore({
variables: { cursor: projectsResult.value?.activeUser?.projects.cursor },
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult || fetchMoreResult.activeUser?.projects.items.length === 0) {
hasReachedEnd.value = true
return previousResult
if (
!fetchMoreResult ||
fetchMoreResult.activeUser?.projects.items.length === 0
) {
hasReachedEnd.value = true;
return previousResult;
}
if (!previousResult.activeUser || !fetchMoreResult.activeUser)
return previousResult
return previousResult;
return {
activeUser: {
@@ -383,12 +419,12 @@ const loadMore = () => {
totalCount: fetchMoreResult?.activeUser?.projects.totalCount,
items: [
...previousResult.activeUser.projects.items,
...fetchMoreResult.activeUser.projects.items
]
}
}
}
}
})
}
...fetchMoreResult.activeUser.projects.items,
],
},
},
};
},
});
};
</script>
+29 -23
View File
@@ -23,7 +23,7 @@
: item.role === 'workspace:guest'
? 'You do not have write access on this workspace.'
: undefined,
disabled: !(item.readOnly || item.role === 'workspace:guest')
disabled: !(item.readOnly || item.role === 'workspace:guest'),
}"
class="flex items-center"
>
@@ -41,45 +41,51 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { workspacesListQuery } from '~/lib/graphql/mutationsAndQueries'
import type { WorkspaceListWorkspaceItemFragment } from 'lib/common/generated/gql/graphql'
import { storeToRefs } from 'pinia'
import { useAccountStore } from '~/store/accounts'
import { ref, computed } from "vue";
import { useQuery } from "@vue/apollo-composable";
import { workspacesListQuery } from "~/lib/graphql/mutationsAndQueries";
import type { WorkspaceListWorkspaceItemFragment } from "~/lib/common/generated/gql/graphql";
import { storeToRefs } from "pinia";
import { useAccountStore } from "~/store/accounts";
const emit = defineEmits<{
(
e: 'update:selectedWorkspace',
e: "update:selectedWorkspace",
value: WorkspaceListWorkspaceItemFragment | undefined
): void
}>()
): void;
}>();
const accountStore = useAccountStore()
const { activeAccount } = storeToRefs(accountStore)
const accountId = computed(() => activeAccount.value.accountInfo.id)
const accountStore = useAccountStore();
const { activeAccount } = storeToRefs(accountStore);
const accountId = computed(() => activeAccount.value.accountInfo.id);
const searchText = ref<string>()
const searchText = ref<string>();
const { result: workspacesResult } = useQuery(
workspacesListQuery,
() => ({
limit: 5,
filter: {
search: (searchText.value || '').trim() || null
}
search: (searchText.value || "").trim() || null,
},
}),
() => ({ clientId: accountId.value, debounce: 500, fetchPolicy: 'network-only' })
)
() => ({
clientId: accountId.value,
debounce: 500,
fetchPolicy: "network-only",
})
);
const workspaces = computed(() => workspacesResult.value?.activeUser?.workspaces.items)
const selectedWorkspace = ref<WorkspaceListWorkspaceItemFragment>()
const workspaces = computed(
() => workspacesResult.value?.activeUser?.workspaces.items
);
const selectedWorkspace = ref<WorkspaceListWorkspaceItemFragment>();
watch(selectedWorkspace, (newVal) => {
emit('update:selectedWorkspace', newVal)
})
emit("update:selectedWorkspace", newVal);
});
// Utility function to check if the user cannot create a workspace
const userCantCreateWorkspace = (item: WorkspaceListWorkspaceItemFragment) =>
(!!item?.role && item.role === 'workspace:guest') || !!item.readOnly
(!!item?.role && item.role === "workspace:guest") || !!item.readOnly;
</script>
+53 -7
View File
@@ -1,7 +1,19 @@
import { omit } from 'lodash-es'
import { baseConfigs, globals, getESMDirname } from '../../eslint.config.mjs'
import withNuxt from './.nuxt/eslint.config.mjs'
import pluginVueA11y from 'eslint-plugin-vuejs-accessibility'
import globals from 'globals'
import { fileURLToPath } from 'url'
import { dirname } from 'path'
import prettierConfig from 'eslint-config-prettier'
import js from '@eslint/js'
/**
* Feed in import.meta.url in your .mjs module to get the equivalent of __dirname
* @param {string} importMetaUrl
*/
export const getESMDirname = (importMetaUrl) => {
return dirname(fileURLToPath(importMetaUrl))
}
const configs = await withNuxt([
{
@@ -62,6 +74,7 @@ const configs = await withNuxt([
'@typescript-eslint/require-await': 'error',
'no-undef': 'off',
'@typescript-eslint/no-empty-object-type': 'off', // too restrictive
'@typescript-eslint/unified-signatures': 'off', // DX sucks in vue event definitions
'@typescript-eslint/no-dynamic-delete': 'off', // too restrictive
'@typescript-eslint/restrict-template-expressions': 'off', // too restrictive
@@ -78,10 +91,7 @@ const configs = await withNuxt([
{
files: ['**/*.vue'],
rules: {
'vue/component-tags-order': [
'error',
{ order: ['docs', 'template', 'script', 'style'] }
],
'vue/block-order': ['error', { order: ['docs', 'template', 'script', 'style'] }],
'vue/require-default-prop': 'off',
'vue/multi-word-component-names': 'off',
'vue/component-name-in-template-casing': [
@@ -116,10 +126,46 @@ const configs = await withNuxt([
'./lib/common/generated/**/*',
'storybook-static',
'.nuxt/**',
'.output/**'
'.output/**',
'**/dist/**',
'**/dist-*/**',
'**/public/**',
'**/events.json',
'**/generated/**/*'
]
},
...baseConfigs
{
files: ['**/*.mjs'],
languageOptions: {
sourceType: 'module'
}
},
{
files: ['**/*.cjs'],
languageOptions: {
sourceType: 'commonjs'
}
},
{
files: ['**/*.{js,mjs,cjs}', '**/.*.{js,mjs,cjs}'],
...js.configs.recommended
},
prettierConfig,
{
rules: {
camelcase: [
1,
{
properties: 'always'
}
],
'no-var': 'error',
'no-alert': 'error',
eqeqeq: 'error',
'prefer-const': 'warn',
'object-shorthand': 'warn'
}
}
])
export default configs
-136
View File
@@ -1,136 +0,0 @@
import { ApolloClient, gql } from '@apollo/client/core'
import { ApolloClients } from '@vue/apollo-composable'
import type { ComputedRef, Ref } from 'vue'
import type { Account } from '~/lib/bindings/definitions/IBasicConnectorBinding'
import { resolveClientConfig } from '~/lib/core/configs/apollo'
export type DUIAccount = {
/** account info coming from the host app */
accountInfo: Account
/** the graphql client; a bit superflous */
client?: ApolloClient<unknown>
/** whether an intial serverinfo query succeeded. */
isValid: boolean
}
export type DUIAccountsState = {
accounts: Ref<DUIAccount[]>
validAccounts: ComputedRef<DUIAccount[]>
refreshAccounts: () => Promise<void>
defaultAccount: ComputedRef<DUIAccount | undefined>
loading: Ref<boolean>
}
const AccountsInjectionKey = 'DUI_ACCOUNTS_STATE'
/**
* Use this composable to set up the account bindings and graphql clients at the top of the app.
* TODO: Properly handle cases when user was not connected to the internet,
* and then actually got connected.
*/
export function useAccountsSetup(): DUIAccountsState {
const app = useNuxtApp()
const $baseBinding = app.$baseBinding
const accounts = ref<DUIAccount[]>([])
const apolloClients = {} as Record<string, ApolloClient<unknown>>
// Tries to connect to the accounts and sets their is valid prop to false if fails.
const testAccounts = async (accs: DUIAccount[]) => {
const accountTestQuery = gql`
query AcccountTestQuery {
serverInfo {
version
name
company
}
}
`
for (const acc of accs) {
if (!acc.client) continue
try {
await acc.client.query({ query: accountTestQuery })
acc.isValid = true
} catch {
// TODO: properly dispose and kill this client. It's unclear how to do it.
acc.isValid = false
// NOTE: we do not want to delete the client, as we might want to "refresh" in
// case the user was not connected to the interweb.
// acc.client.disableNetworkFetches = true
// acc.client.stop()
// delete acc.client
}
}
}
const loading = ref(false)
// Matches local accounts coming from the host app to app state.
const refreshAccounts = async () => {
loading.value = true
const accs = await $baseBinding.getAccounts()
// We create a whole new list of accounts that will replace the old list. This way we ensure we drop
// out of scope old accounts that not exist anymore (TODO: test), and we don't need to do complex diffing.
const newAccs = [] as DUIAccount[]
for (const acc of accs) {
const existing = accounts.value.find((a) => a.accountInfo.id === acc.id)
if (existing) {
newAccs.push(existing as DUIAccount)
continue
}
const client = new ApolloClient(
resolveClientConfig({
httpEndpoint: new URL('/graphql', acc.serverInfo.url).href,
authToken: () => acc.token
})
)
apolloClients[acc.id] = client
newAccs.push({
accountInfo: acc,
client,
isValid: true
})
}
// We test accounts here so we try to prevent the app from querying/using invalid accounts.
await testAccounts(newAccs)
// Once we have tested the new accounts, finally set them.
accounts.value = newAccs
loading.value = false
}
void refreshAccounts() // Promise that we do not want to await (convention with void)
const defaultAccount = computed(() =>
accounts.value.find((acc) => acc.accountInfo.isDefault)
)
const validAccounts = computed(() => {
return accounts.value.filter((a) => a.isValid)
})
const accState = {
accounts,
defaultAccount,
validAccounts,
refreshAccounts,
loading
}
app.vueApp.provide(ApolloClients, apolloClients)
provide(AccountsInjectionKey, accState)
return accState // as DUIAccountsState
}
/**
* Use this composable to access the users' local accounts and their corresponding graphql client.
*/
export function useInjectedAccounts(): DUIAccountsState {
const state = inject(AccountsInjectionKey) as DUIAccountsState
return state
}
+22 -19
View File
@@ -1,30 +1,33 @@
import type { IBinding, IBindingSharedEvents } from 'lib/bindings/definitions/IBinding'
import type {
IBinding,
IBindingSharedEvents,
} from "~/lib/bindings/definitions/IBinding";
export const IAccountBindingKey = 'accountsBinding'
export const IAccountBindingKey = "accountsBinding";
export interface IAccountBinding extends IBinding<IAccountBindingEvents> {
getAccounts: () => Promise<Account[]>
removeAccount: (accountId: string) => Promise<void>
getAccounts: () => Promise<Account[]>;
removeAccount: (accountId: string) => Promise<void>;
}
// An almost 1-1 mapping of what we need from the Core accounts class.
export type Account = {
id: string
isDefault: boolean
token: string
id: string;
isDefault: boolean;
token: string;
serverInfo: {
name: string
url: string
frontend2: boolean
}
name: string;
url: string;
frontend2: boolean;
};
userInfo: {
id: string
avatar: string
email: string
name: string
commits: { totalCount: number }
streams: { totalCount: number }
}
}
id: string;
avatar: string;
email: string;
name: string;
commits: { totalCount: number };
streams: { totalCount: number };
};
};
export interface IAccountBindingEvents extends IBindingSharedEvents {}
+13 -13
View File
@@ -1,17 +1,17 @@
import type { ConversionResult } from 'lib/conversions/conversionResult'
import type { IModelCardSharedEvents } from '~/lib/models/card'
import type { CardSetting } from '~/lib/models/card/setting'
import type { ConversionResult } from "~/lib/conversions/conversionResult";
import type { IModelCardSharedEvents } from "~/lib/models/card";
import type { CardSetting } from "~/lib/models/card/setting";
import type {
IBinding,
IBindingSharedEvents
} from '~~/lib/bindings/definitions/IBinding'
IBindingSharedEvents,
} from "~~/lib/bindings/definitions/IBinding";
export const IReceiveBindingKey = 'receiveBinding'
export const IReceiveBindingKey = "receiveBinding";
export interface IReceiveBinding extends IBinding<IReceiveBindingEvents> {
receive: (modelCardId: string) => Promise<void>
getReceiveSettings: () => Promise<CardSetting[]>
cancelReceive: (modelId: string) => Promise<void>
receive: (modelCardId: string) => Promise<void>;
getReceiveSettings: () => Promise<CardSetting[]>;
cancelReceive: (modelId: string) => Promise<void>;
}
export interface IReceiveBindingEvents
@@ -19,8 +19,8 @@ export interface IReceiveBindingEvents
IModelCardSharedEvents {
// See note oon timeout in bridge v2; we might not need this
setModelReceiveResult: (args: {
modelCardId: string
bakedObjectIds: string[]
conversionResults: ConversionResult[]
}) => void
modelCardId: string;
bakedObjectIds: string[];
conversionResults: ConversionResult[];
}) => void;
}
+24 -24
View File
@@ -1,40 +1,40 @@
import type { ISendFilter } from '~~/lib/models/card/send'
import type { ISendFilter } from "~~/lib/models/card/send";
import type {
IBinding,
IBindingSharedEvents
} from '~~/lib/bindings/definitions/IBinding'
import type { CardSetting } from '~/lib/models/card/setting'
import type { IModelCardSharedEvents } from '~/lib/models/card'
import type { ConversionResult } from 'lib/conversions/conversionResult'
import type { CreateVersionArgs } from '~/lib/bridge/server'
IBindingSharedEvents,
} from "~~/lib/bindings/definitions/IBinding";
import type { CardSetting } from "~/lib/models/card/setting";
import type { IModelCardSharedEvents } from "~/lib/models/card";
import type { ConversionResult } from "~/lib/conversions/conversionResult";
import type { CreateVersionArgs } from "~/lib/bridge/server";
export const ISendBindingKey = 'sendBinding'
export const ISendBindingKey = "sendBinding";
export interface ISendBinding extends IBinding<ISendBindingEvents> {
getSendFilters: () => Promise<ISendFilter[]>
getSendSettings: () => Promise<CardSetting[]>
send: (modelId: string) => Promise<void>
cancelSend: (modelId: string) => Promise<void>
getSendFilters: () => Promise<ISendFilter[]>;
getSendSettings: () => Promise<CardSetting[]>;
send: (modelId: string) => Promise<void>;
cancelSend: (modelId: string) => Promise<void>;
}
export interface ISendBindingEvents
extends IBindingSharedEvents,
IModelCardSharedEvents {
refreshSendFilters: () => void
setModelsExpired: (modelCardIds: string[]) => void
refreshSendFilters: () => void;
setModelsExpired: (modelCardIds: string[]) => void;
setModelSendResult: (args: {
modelCardId: string
versionId: string
sendConversionResults: ConversionResult[]
}) => void
modelCardId: string;
versionId: string;
sendConversionResults: ConversionResult[];
}) => void;
setIdMap: (args: {
modelCardId: string
idMap: Record<string, string>
newSelectedObjectIds: string[]
}) => void
modelCardId: string;
idMap: Record<string, string>;
newSelectedObjectIds: string[];
}) => void;
/**
* Use whenever want to cancel model card progress, it is used on Archicad so far since send operation blocks the UI thread.
*/
triggerCancel: (modelCardId: string) => void
triggerCreateVersion: (args: CreateVersionArgs) => void
triggerCancel: (modelCardId: string) => void;
triggerCreateVersion: (args: CreateVersionArgs) => void;
}
+39 -39
View File
@@ -1,54 +1,54 @@
import crs from 'crypto-random-string'
import type { AutomationRunItemFragment } from 'lib/common/generated/gql/graphql'
import type { ConversionResult } from 'lib/conversions/conversionResult'
import type { CardSetting } from 'lib/models/card/setting'
import type { IDiscriminatedObject } from '~~/lib/bindings/definitions/common'
import { DiscriminatedObject } from '~~/lib/bindings/definitions/common'
import crs from "crypto-random-string";
import type { AutomationRunItemFragment } from "~/lib/common/generated/gql/graphql";
import type { ConversionResult } from "~/lib/conversions/conversionResult";
import type { CardSetting } from "~/lib/models/card/setting";
import type { IDiscriminatedObject } from "~~/lib/bindings/definitions/common";
import { DiscriminatedObject } from "~~/lib/bindings/definitions/common";
export interface IModelCard extends IDiscriminatedObject {
modelCardId: string
modelId: string
projectId: string
workspaceId?: string
workspaceSlug?: string
accountId: string
serverUrl: string
expired: boolean
progress?: ModelCardProgress
settings?: CardSetting[]
error?: { errorMessage: string; dismissible: boolean }
report?: ConversionResult[]
automationRuns?: AutomationRunItemFragment[]
modelCardId: string;
modelId: string;
projectId: string;
workspaceId?: string;
workspaceSlug?: string;
accountId: string;
serverUrl: string;
expired: boolean;
progress?: ModelCardProgress;
settings?: CardSetting[];
error?: { errorMessage: string; dismissible: boolean };
report?: ConversionResult[];
automationRuns?: AutomationRunItemFragment[];
}
export class ModelCard extends DiscriminatedObject implements IModelCard {
modelCardId: string
modelId!: string
projectId!: string
workspaceId?: string
workspaceSlug?: string
accountId!: string
serverUrl!: string
expired: boolean
progress: ModelCardProgress | undefined
settings: CardSetting[] | undefined
modelCardId: string;
modelId!: string;
projectId!: string;
workspaceId?: string;
workspaceSlug?: string;
accountId!: string;
serverUrl!: string;
expired: boolean;
progress: ModelCardProgress | undefined;
settings: CardSetting[] | undefined;
constructor(typeDiscriminator: string) {
super(typeDiscriminator)
this.modelCardId = crs({ length: 20 })
this.expired = false
super(typeDiscriminator);
this.modelCardId = crs({ length: 20 });
this.expired = false;
}
}
export interface IModelCardSharedEvents {
setModelError: (args: { modelCardId: string; error: string }) => void
setModelError: (args: { modelCardId: string; error: string }) => void;
setModelProgress: (args: {
modelCardId: string
progress?: ModelCardProgress
}) => void
modelCardId: string;
progress?: ModelCardProgress;
}) => void;
}
export type ModelCardProgress = {
status: string
progress?: number
}
status: string;
progress?: number;
};
+11 -11
View File
@@ -1,18 +1,18 @@
import type { ConversionResult } from 'lib/conversions/conversionResult'
import type { ConversionResult } from "~/lib/conversions/conversionResult";
export type ModelCardNotification = {
modelCardId: string
text: string
level: 'info' | 'danger' | 'warning' | 'success'
modelCardId: string;
text: string;
level: "info" | "danger" | "warning" | "success";
cta?: {
name: string
action: () => void
}
name: string;
action: () => void;
};
/**
* If set, will display a view report button next to cta
*/
report?: ConversionResult[]
report?: ConversionResult[];
// TODO figure out re report button
dismissible: boolean
timeout?: number
}
dismissible: boolean;
timeout?: number;
};
+5 -3
View File
@@ -55,11 +55,11 @@
"devDependencies": {
"@graphql-codegen/cli": "^5.0.5",
"@graphql-codegen/client-preset": "^4.3.0",
"@nuxt/eslint": "^0.3.13",
"@nuxt/eslint": "^1.3.1",
"@nuxtjs/tailwindcss": "^6.14.0",
"@parcel/watcher": "^2.5.1",
"@types/apollo-upload-client": "^17.0.1",
"@types/eslint": "^8.56.10",
"@types/eslint": "^9.6.1",
"@types/lodash-es": "^4.17.6",
"@types/node": "^18",
"@typescript-eslint/eslint-plugin": "^8.20.0",
@@ -89,7 +89,9 @@
},
"resolutions": {
"core-js": "3.22.4",
"core-js-compat/semver": "^7.5.4"
"core-js-compat/semver": "^7.5.4",
"@babel/plugin-transform-classes/globals": "13.13.0",
"@babel/traverse/globals": "13.13.0"
},
"packageManager": "yarn@4.9.1"
}
+2 -3
View File
@@ -15,7 +15,6 @@ import { onError, type ErrorResponse } from '@apollo/client/link/error'
import { getMainDefinition } from '@apollo/client/utilities'
import { setContext } from '@apollo/client/link/context'
import { useHostAppStore } from '~/store/hostApp'
import type { ToastNotification } from '@speckle/ui-components'
import { ToastNotificationType } from '@speckle/ui-components'
export type DUIAccount = {
@@ -86,7 +85,7 @@ export const useAccountStore = defineStore('accountStore', () => {
try {
await acc.client.query({ query: accountTestQuery })
acc.isValid = true
} catch (error) {
} catch {
// TODO: properly dispose and kill this client. It's unclear how to do it.
acc.isValid = false
// NOTE: we do not want to delete the client, as we might want to "refresh" in
@@ -116,7 +115,7 @@ export const useAccountStore = defineStore('accountStore', () => {
if (res.graphQLErrors) {
if (
res.graphQLErrors?.some(
(err) => err.extensions.code === 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR'
(err) => err.extensions?.code === 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR'
)
) {
hostAppStore.setNotification({
+29 -29
View File
@@ -1,50 +1,50 @@
import type { ConnectorConfig } from 'lib/bindings/definitions/IConfigBinding'
import { defineStore } from 'pinia'
import type { ConnectorConfig } from "~/lib/bindings/definitions/IConfigBinding";
import { defineStore } from "pinia";
export const useConfigStore = defineStore('configStore', () => {
const { $configBinding } = useNuxtApp()
export const useConfigStore = defineStore("configStore", () => {
const { $configBinding } = useNuxtApp();
const hasConfigBindings = ref(!!$configBinding)
const hasConfigBindings = ref(!!$configBinding);
const userSelectedWorkspaceId = ref<string>()
const userSelectedWorkspaceId = ref<string>();
const config = ref<ConnectorConfig>({ darkTheme: true })
const config = ref<ConnectorConfig>({ darkTheme: true });
const isDarkTheme = computed(() => {
return config.value?.darkTheme
})
const isDevMode = ref(false)
return config.value?.darkTheme;
});
const isDevMode = ref(false);
const toggleTheme = () => {
config.value.darkTheme = !config.value.darkTheme
$configBinding.updateConfig(config.value)
}
config.value.darkTheme = !config.value.darkTheme;
$configBinding.updateConfig(config.value);
};
const setUserSelectedWorkspace = (workspaceId: string) => {
userSelectedWorkspaceId.value = workspaceId
userSelectedWorkspaceId.value = workspaceId;
try {
$configBinding.setUserSelectedWorkspaceId(workspaceId)
$configBinding.setUserSelectedWorkspaceId(workspaceId);
} catch (error) {
console.warn(error) // for the users who do not have latest version with workspace config handling
console.warn(error); // for the users who do not have latest version with workspace config handling
}
}
};
const isInitialized = ref(false)
const isInitialized = ref(false);
const init = async () => {
if (!$configBinding) return
config.value = await $configBinding.getConfig()
const workspacesConfig = await $configBinding.getWorkspacesConfig()
if (!$configBinding) return;
config.value = await $configBinding.getConfig();
const workspacesConfig = await $configBinding.getWorkspacesConfig();
if (workspacesConfig && workspacesConfig.userSelectedWorkspaceId) {
userSelectedWorkspaceId.value = workspacesConfig.userSelectedWorkspaceId
userSelectedWorkspaceId.value = workspacesConfig.userSelectedWorkspaceId;
}
}
init()
};
init();
const getIsDevMode = async () =>
(isDevMode.value = await $configBinding.getIsDevMode())
(isDevMode.value = await $configBinding.getIsDevMode());
void getIsDevMode()
void getIsDevMode();
return {
isInitialized,
@@ -54,6 +54,6 @@ export const useConfigStore = defineStore('configStore', () => {
isDevMode,
userSelectedWorkspaceId,
toggleTheme,
setUserSelectedWorkspace
}
})
setUserSelectedWorkspace,
};
});
+335 -311
View File
File diff suppressed because it is too large Load Diff
+540 -807
View File
File diff suppressed because it is too large Load Diff