feat(frontend): comments in viewer embed + refactored frontend viewer foundations

This commit is contained in:
Fabians
2022-07-27 16:42:08 +03:00
parent 5917e02a05
commit 69a10f7f08
87 changed files with 2894 additions and 1995 deletions
-2
View File
@@ -6,8 +6,6 @@
We're working to stabilize the 2.0 API, and until then there will be breaking changes.
Note that this package contains two vue apps, the main frontend (located under @/main), and the viewer embed app (@/embed).
Notes:
- In **development** mode, the Speckle Server will proxy the frontend from `localhost:3000` to `localhost:8080`. If you don't see anything, ensure you've run `yarn serve` in the frontend package.
+4 -1
View File
@@ -1,5 +1,7 @@
overwrite: true
schema: 'http://localhost:3000/graphql'
schema:
- 'http://localhost:3000/graphql'
- 'src/graphql/local-only-schema/schema.gql'
documents:
- 'src/graphql/**/*.gql'
- 'src/**/*.{ts,tsx,js,jsx,vue}'
@@ -13,3 +15,4 @@ generates:
config:
scalars:
JSONObject: Record<string, unknown>
UnknownLocalObject: unknown
@@ -82,12 +82,6 @@ server {
expires 1y;
}
location /embed {
default_type text/html;
alias /usr/share/nginx/html/embedApp.html;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
location ~ ^/streams/.* {
default_type text/html;
content_by_lua_block {
+3 -4
View File
@@ -3,7 +3,7 @@
"version": "2.5.4",
"private": true,
"scripts": {
"serve": "ws -p 8080 -d dist -r '/embed(.*) -> /embedApp.html' '/([a-zA-Z0-9-_/]*)(\\?.*)? -> /app.html' ",
"serve": "ws -p 8080 -d dist -r '/([a-zA-Z0-9-_/]*)(\\?.*)? -> /app.html' ",
"build": "vue-cli-service build --mode production --silent",
"lint": "eslint . --ext .js,.ts,.vue,.tsx,.jsx",
"lint:vue": "vti diagnostics",
@@ -38,6 +38,7 @@
"@vue/apollo-option": "^4.0.0-alpha.20",
"@vuejs-community/vue-filter-date-format": "^1.6.3",
"@vuejs-community/vue-filter-date-parse": "^1.1.6",
"@vueuse/core": "^9.0.0",
"apexcharts": "^3.33.1",
"apollo-upload-client": "^17.0.0",
"dompurify": "^2.3.6",
@@ -61,8 +62,7 @@
"vue2-perfect-scrollbar": "^1.5.2",
"vuedraggable": "^2.24.3",
"vuetify": "^2.3.21",
"vuetify-image-input": "^19.1.0",
"vuex": "^3.6.2"
"vuetify-image-input": "^19.1.0"
},
"devDependencies": {
"@graphql-codegen/cli": "2.6.2",
@@ -84,7 +84,6 @@
"@vue/cli-plugin-babel": "~4.3.1",
"@vue/cli-plugin-router": "~4.3.1",
"@vue/cli-plugin-typescript": "~4.5.19",
"@vue/cli-plugin-vuex": "~4.3.1",
"@vue/cli-service": "~4.3.1",
"@vue/eslint-config-typescript": "^11.0.0",
"babel-eslint": "^10.1.0",
-147
View File
@@ -1,147 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- <base href="/appname/"> -->
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"
/>
<style type="text/css">
/* body {
background-color: #333333;
color: #0a66ff;
}
@media screen and (prefers-color-scheme: light) {
body {
background-color: white;
color: #0a66ff;
}
} */
.hover-tada:hover {
-webkit-animation-name: tada;
animation-name: tada;
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
animation-iteration-count: infinite;
}
.tada {
-webkit-animation-name: tada;
animation-name: tada;
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
animation-iteration-count: infinite;
}
@-webkit-keyframes tada {
0% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%,
20% {
-webkit-transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
}
30%,
50%,
70%,
90% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
}
40%,
60%,
80% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
}
100% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
@keyframes tada {
0% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%,
20% {
-webkit-transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
}
30%,
50%,
70%,
90% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
}
40%,
60%,
80% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
}
100% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
</style>
</head>
<body>
<noscript>
<strong>
We're sorry but Speckle doesn't work properly without JavaScript enabled. Please
enable it to continue.
</strong>
</noscript>
<div id="app">
<div
style="
width: 100%;
height: 300px;
font-family: sans-serif !important;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
text-align: center;
font-weight: 400;
font-size: 10px;
"
>
<img src="<%= BASE_URL %>logo.svg" style="max-width: 50px" class="tada" />
</div>
</div>
<!-- built files will be auto injected -->
</body>
</html>
+2 -13
View File
@@ -5,25 +5,13 @@ import PortalVue from 'portal-vue'
import { formatNumber } from '@/plugins/formatNumber'
/**
* Global bootstrapping that is used in all of the frontend apps (main/embed)
* Global bootstrapping for the frontend app
*/
// Filter to turn any number into a nice string like '10k', '5.5m'
// Accepts 'max' parameter to set it's formatting while being animated
Vue.filter('prettynum', formatNumber)
// Async HistogramSlider load
// TODO: Instead of bundling it globally on all pages, only import it where needed
Vue.component('HistogramSlider', async () => {
await import(
/* webpackChunkName: "vue-histogram-slider" */ 'vue-histogram-slider/dist/histogram-slider.css'
)
const component = await import(
/* webpackChunkName: "vue-histogram-slider" */ 'vue-histogram-slider'
)
return component
})
// process.env.NODE_ENV is injected by Webpack
Vue.config.productionTip = process.env.NODE_ENV === 'development'
@@ -33,6 +21,7 @@ Vue.use(VTooltip, {
defaultHtml: false
})
// In highly restrictive sandboxed environments mixpanel init might fail due to document.cookie access
Vue.use(VueMixpanel, {
token: 'acd87c5a50b56df91a795e999812a3a4',
config: {
+97 -76
View File
@@ -1,6 +1,12 @@
import Vue from 'vue'
import { createApolloProvider, ApolloProvider } from '@vue/apollo-option'
import { ApolloClient, ApolloLink, InMemoryCache, split } from '@apollo/client/core'
import {
ApolloClient,
ApolloLink,
InMemoryCache,
split,
TypePolicies
} from '@apollo/client/core'
import { setContext } from '@apollo/client/link/context'
import { WebSocketLink } from '@apollo/client/link/ws'
import { SubscriptionClient } from 'subscriptions-transport-ws'
@@ -13,6 +19,8 @@ import {
buildAbstractCollectionMergeFunction,
incomingOverwritesExistingMergeFunction
} from '@/main/lib/core/helpers/apolloSetupHelper'
import { merge } from 'lodash'
import { statePolicies as commitObjectViewerStatePolicies } from '@/main/lib/viewer/commit-object-viewer/stateManager'
// Name of the localStorage item
const AUTH_TOKEN = LocalStorageKeys.AuthToken
@@ -38,88 +46,101 @@ function createCache(): InMemoryCache {
*
* Read more: https://www.apollographql.com/docs/react/caching/cache-field-behavior
*/
typePolicies: {
Query: {
fields: {
user: {
read(original, { args, toReference }) {
if (args?.id) {
return toReference({ __typename: 'User', id: args.id })
}
typePolicies: merge<TypePolicies, TypePolicies>(
{
Query: {
fields: {
user: {
read(original, { args, toReference }) {
if (args?.id) {
return toReference({ __typename: 'User', id: args.id })
}
return original
}
},
stream: {
read(original, { args, toReference }) {
if (args?.id) {
return toReference({ __typename: 'Stream', id: args.id })
return original
}
},
stream: {
read(original, { args, toReference }) {
if (args?.id) {
return toReference({ __typename: 'Stream', id: args.id })
}
return original
return original
}
},
streams: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('StreamCollection', {
checkIdentity: true
})
}
},
streams: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('StreamCollection')
}
},
User: {
fields: {
timeline: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
},
commits: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('CommitCollectionUser', {
checkIdentity: true
})
},
favoriteStreams: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('StreamCollection', {
checkIdentity: true
})
}
}
},
Stream: {
fields: {
activity: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
},
commits: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
checkIdentity: true
})
},
pendingCollaborators: {
merge: incomingOverwritesExistingMergeFunction
}
}
},
Branch: {
fields: {
commits: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
checkIdentity: true
})
}
}
},
BranchCollection: {
merge: true
},
ServerStats: {
merge: true
},
WebhookEventCollection: {
merge: true
},
ServerInfo: {
merge: true
},
CommentThreadActivityMessage: {
merge: true
}
},
User: {
fields: {
timeline: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
},
commits: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('CommitCollectionUser')
},
favoriteStreams: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('StreamCollection')
}
}
},
Stream: {
fields: {
activity: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
},
commits: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('CommitCollection')
},
pendingCollaborators: {
merge: incomingOverwritesExistingMergeFunction
}
}
},
Branch: {
fields: {
commits: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('CommitCollection')
}
}
},
BranchCollection: {
merge: true
},
ServerStats: {
merge: true
},
WebhookEventCollection: {
merge: true
},
ServerInfo: {
merge: true
},
CommentThreadActivityMessage: {
merge: true
}
}
commitObjectViewerStatePolicies
)
})
}
-9
View File
@@ -1,9 +0,0 @@
<template lang="html">
<router-view />
</template>
<script>
export default {
components: {},
mounted() {}
}
</script>
-269
View File
@@ -1,269 +0,0 @@
<template>
<v-app
:class="`embed-viewer no-scrollbar ${
$route.query.transparent === 'true'
? ''
: $vuetify.theme.dark
? 'background-dark'
: 'background-light'
}`"
>
<!-- BG image -->
<div
v-if="objectIdsToLoad.length !== 0 && !isModelLoaded"
style="position: fixed; top: 0; width: 100%; height: 100%; cursor: pointer"
@click="load()"
>
<preview-image
:url="`/preview/${$route.query.stream}/objects/${objectIdsToLoad[0]}`"
:height="height"
rotate
></preview-image>
</div>
<!-- Play button -->
<div
v-if="!isModelLoaded && !error"
class="viewer-play d-flex fullscreen align-center justify-center no-mouse"
>
<v-btn
id="viewer-play-btn"
:disabled="showPlayLoader"
fab
color="primary"
class="elevation-4 hover-tada mouse"
@click="load()"
>
<v-icon v-if="!showPlayLoader">mdi-play</v-icon>
<v-icon v-else class="spinning-icon">mdi-loading</v-icon>
</v-btn>
</div>
<!-- Async loaded viewer -->
<embed-viewer-core
v-if="shouldLoadHeavyDeps"
:objects="objectIdsToLoad"
@model-loaded="onModelLoaded"
@error="onError"
/>
<!-- Display error if needed -->
<div v-if="error" class="fullscreen d-flex justify-center align-center">
<div class="">
<p class="text-h5 text-center red--text">Speckle Embedding Error</p>
<p class="text-center grey--text">
Double check to see if the stream is public and if the embed link is correct.
</p>
</div>
</div>
</v-app>
</template>
<script lang="ts">
import { Nullable } from '@/helpers/typeHelpers'
import { getStreamObj, getBranchObj, getCommitObj } from '@/embed/speckleUtils'
import Vue from 'vue'
/**
* TODO:
* - Move EmbedViewer back to main app? The main app has a lot of global dependencies
* that this endpoint doesn't need, tho...
* - Make speckle-viewer configurable through props/events not through window.__viewer
*/
export default Vue.extend({
name: 'EmbedViewer',
components: {
EmbedViewerCore: () => import('@/embed/EmbedViewerCore.vue'),
PreviewImage: () => import('@/main/components/common/PreviewImage.vue')
},
data() {
return {
isModelLoaded: false,
error: null as Nullable<Error>,
displayType: 'stream',
objectIdsToLoad: [] as any[],
input: {
stream: this.$route.query.stream,
object: this.$route.query.object,
branch: this.$route.query.branch || 'main',
commit: this.$route.query.commit,
overlay: this.$route.query.overlay,
camera: this.$route.query.c,
filter: this.$route.query.filter
} as Record<string, string>,
isInitialized: false as boolean,
shouldLoadHeavyDeps: false as boolean,
height: window.innerHeight
}
},
computed: {
streamId(): string {
return this.$route.query.stream as string
},
objectUrl(): string {
return `${window.location.protocol}//${window.location.host}/streams/${this.input.stream}/objects/${this.input.object}`
},
showPlayLoader(): boolean {
return !this.isInitialized || (this.shouldLoadHeavyDeps && !this.isModelLoaded)
}
},
mounted() {
if (this.$route.query.transparent === 'true') {
document.getElementById('app')!.classList.remove('theme--dark')
document.getElementById('app')!.classList.remove('theme--light')
}
window.addEventListener('resize', () => {
this.height = window.innerHeight
})
},
async beforeMount() {
if (this.$route.query.stream) this.displayType = 'stream'
if (this.$route.query.branch) this.displayType = 'branch'
if (this.$route.query.commit) this.displayType = 'commit'
if (this.$route.query.object) this.displayType = 'object'
if (this.$route.query.overlay) this.displayType = 'multiple'
try {
switch (this.displayType) {
case 'stream': {
const res = await getStreamObj(this.$route.query.stream)
if (res.data.stream.commits.totalCount === 0)
throw new Error('Stream has no commits.')
this.objectIdsToLoad.push(res.data.stream.commits.items[0].referencedObject)
break
}
case 'branch': {
const res = await getBranchObj(
this.$route.query.stream,
this.$route.query.branch
)
if (res.data.stream.branch.commits.totalCount === 0)
throw new Error('Branch has no commits.')
this.objectIdsToLoad.push(
res.data.stream.branch.commits.items[0].referencedObject
)
break
}
case 'commit': {
const res = await getCommitObj(
this.$route.query.stream,
this.$route.query.commit
)
this.objectIdsToLoad.push(res.data.stream.commit.referencedObject)
break
}
case 'object':
this.objectIdsToLoad.push(this.$route.query.object)
break
case 'multiple': {
if (this.$route.query.commit) {
const res = await getCommitObj(
this.$route.query.stream,
this.$route.query.commit
)
this.objectIdsToLoad.push(res.data.stream.commit.referencedObject)
} else {
this.objectIdsToLoad.push(this.$route.query.object)
}
for (const resId of (this.$route.query.overlay as string).split(',')) {
if (resId.length === 10) {
const res = await getCommitObj(this.$route.query.stream, resId)
this.objectIdsToLoad.push(res.data.stream.commit.referencedObject)
} else {
this.objectIdsToLoad.push(resId)
}
}
break
}
default:
break
}
// Mark as initialized (enable play button)
this.isInitialized = true
} catch (e: unknown) {
this.error = e instanceof Error ? e : new Error('Unexpected error')
}
},
methods: {
onError(e: unknown) {
this.error = e instanceof Error ? e : new Error('Unexpected error')
},
onModelLoaded() {
this.isModelLoaded = true
},
load() {
if (!this.isInitialized || this.shouldLoadHeavyDeps || this.isModelLoaded) return
this.shouldLoadHeavyDeps = true
this.$mixpanel.track('Embedded Model Load', {
type: 'action'
})
}
}
})
</script>
<style lang="scss">
body::-webkit-scrollbar {
display: none;
}
.fullscreen {
height: 100vh !important;
width: 100vw !important;
position: fixed;
&::-webkit-scrollbar {
display: none;
}
top: 0;
left: 0;
z-index: 10;
}
.no-scrollbar {
width: 100vw;
height: 100vh;
overflow: hidden;
&::-webkit-scrollbar {
display: none;
}
}
.bg-img {
background-position: center;
background-repeat: no-repeat;
/*background-attachment: fixed;*/
filter: blur(2px);
}
#viewer-play-btn {
@keyframes spinner-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
// kinda hacky, but vuetify renders the disabled state in a stupid way that relies
// on the background color of the thing behind the button
&.v-btn--fab.v-btn--disabled {
background-color: #242424 !important;
}
.spinning-icon {
animation: spinner-spin 0.5s linear infinite;
}
}
.no-mouse {
pointer-events: none;
}
.mouse {
pointer-events: auto;
}
</style>
@@ -1,234 +0,0 @@
<template>
<div class="embed-viewer-core">
<!-- Viewer navbar (position fixed) -->
<div v-if="!error" style="z-index: 100">
<div
class="top-left bottom-left pa-4 d-flex justify-space-between"
style="right: 0px; position: fixed; z-index: 1000; width: 100%"
>
<v-btn fab small style="z-index=1000" @click="drawer = !drawer">
<v-icon>mdi-menu</v-icon>
</v-btn>
<span v-show="!drawer" class="caption d-inline-flex align-center">
<img src="@/assets/logo.svg" height="18" />
<span style="margin-top: 2px" class="primary--text">
<a href="https://speckle.xyz" target="_blank" class="text-decoration-none">
<b>Powered by Speckle</b>
</a>
</span>
</span>
</div>
<div
v-show="!drawer && loadedModel"
class="caption grey--text pa-2"
style="z-index=1000"
></div>
<div
class="pa-2 d-flex align-center justify-space-between caption"
style="position: fixed; bottom: 0; width: 100%"
>
<portal to="viewercontrols">
<v-btn
v-tooltip="'View extra details in Speckle!'"
icon
dark
large
class="elevation-5 primary pa-0 ma-o"
:href="goToServerUrl"
target="blank"
>
<v-icon dark small>mdi-open-in-new</v-icon>
</v-btn>
</portal>
</div>
<div
:style="`width: 100%; bottom: 12px; left: 0px; position: ${
$isMobile() ? 'fixed' : 'absolute'
}; z-index: 20`"
:class="`d-flex justify-center`"
>
<viewer-controls v-show="loadedModel" />
</div>
</div>
<!-- Model loading progress bar -->
<div
v-if="!loadedModel && loadProgress > 0"
class="viewer-loader d-flex fullscreen align-center justify-center"
>
<v-progress-linear
v-model="loadProgress"
:indeterminate="loadProgress >= 99 && !loadedModel"
color="primary"
style="max-width: 30%"
></v-progress-linear>
</div>
<!-- Viewer filters panel / sidebar -->
<v-navigation-drawer
v-model="drawer"
class="viewer-controls-drawer"
app
floating
disable-resize-watcher
style="z-index: 10000"
>
<div class="px-1 pt-1 d-flex flex-column" style="height: 100%; width: 100%">
<!-- Drawer closer -->
<v-btn icon small class="align-self-end mb-2" @click="drawer = false">
<v-icon x-small>mdi-close</v-icon>
</v-btn>
<!-- Views display -->
<views-display v-if="views.length !== 0" :views="views" :sticky-top="false" />
<!-- Filters display -->
<viewer-filters
:props="objectProperties"
style="width: 100%"
:sticky-top="false"
/>
</div>
</v-navigation-drawer>
<!-- Actual viewer -->
<div style="position: fixed" class="viewer-wrapper no-scrollbar fullscreen">
<speckle-viewer @load-progress="captureProgress" @viewer-init="onViewerInit" />
</div>
</div>
</template>
<script lang="ts">
import { Nullable } from '@/helpers/typeHelpers'
import Vue from 'vue'
import SpeckleViewer from '@/main/components/common/SpeckleViewer.vue'
import ViewerControls from '@/main/components/viewer/ViewerControls.vue'
import ViewsDisplay from '@/main/components/viewer/ViewsDisplay.vue'
import ViewerFilters from '@/main/components/viewer/ViewerFilters.vue'
/**
* Core embed viewer functionality with all of the heavy JS dependencies has been extracted to this component,
* so that we can lazy-load it only when the user clicks on the "play" button
*
* Make sure this component isn't initialized until it's actually needed and don't put any heavy deps
* inside EmbedViewer.vue (or asynchronize them)
*/
type UnknownObject = Record<string, unknown>
export default Vue.extend({
name: 'EmbedViewerCore',
components: {
SpeckleViewer,
ViewerControls,
ViewsDisplay,
ViewerFilters
},
props: {
objects: {
type: Array,
default: () => []
}
},
data() {
return {
error: null as Nullable<Error>,
drawer: false,
loadedModel: false,
loadProgress: 0,
views: [] as Array<UnknownObject>,
objectProperties: null as Nullable<UnknownObject>
}
},
computed: {
goToServerUrl(): string {
const base = `${window.location.origin}/streams/${this.$route.query.stream}/`
if (this.$route.query.commit) return base + `commits/${this.$route.query.commit}`
if (this.$route.query.object) return base + `objects/${this.$route.query.object}`
if (this.$route.query.branch)
return base + `branches/${encodeURI(this.$route.query.branch as string)}`
return base
}
},
watch: {
error(newVal: Error | null) {
if (newVal) this.$emit('error', newVal)
}
},
methods: {
captureProgress(args: { progress: number; id: number; url: string }) {
this.loadProgress = args.progress * 100
},
markModelLoaded() {
this.loadedModel = true
this.$emit('model-loaded')
},
async onViewerInit() {
if (!window.__viewer) {
throw new Error('Viewer instance unavailable')
}
for (const id of this.objects)
await window.__viewer.loadObject(
`${window.location.origin}/streams/${this.$route.query.stream}/objects/${id}`
)
window.__viewer.zoomExtents(undefined, true)
this.markModelLoaded()
this.views.push(...window.__viewer.sceneManager.views)
this.objectProperties = await window.__viewer.getObjectsProperties()
if (this.$route.query.filter) {
const parsedFilter = JSON.parse(this.$route.query.filter as string)
setTimeout(() => {
this.$store.commit('setFilterDirect', { filter: parsedFilter })
}, 1000)
}
if (this.$route.query.c) {
const cam = JSON.parse(this.$route.query.c as string)
window.__viewer.interactions.setLookAt(
{ x: cam[0], y: cam[1], z: cam[2] }, // position
{ x: cam[3], y: cam[4], z: cam[5] } // target
)
}
}
}
})
</script>
<style lang="scss">
.embed-viewer-core {
position: relative;
width: 100%;
height: 100%;
.fullscreen {
height: 100vh !important;
width: 100vw !important;
position: fixed;
&::-webkit-scrollbar {
display: none;
}
top: 0;
left: 0;
z-index: 1;
}
.no-scrollbar {
width: 100vw;
height: 100vh;
overflow: hidden;
&::-webkit-scrollbar {
display: none;
}
}
}
</style>
-23
View File
@@ -1,23 +0,0 @@
import '@/bootstrapper'
import Vue from 'vue'
import App from './EmbedApp.vue'
import vuetify from './embedVuetify'
import router from './embedRouter'
import '@/plugins/helpers'
import store from '@/main/store'
import * as MixpanelManager from '@/mixpanelManager'
// Init mixpanel
MixpanelManager.initialize({
hostApp: 'web-embed',
hostAppDisplayName: 'Embed App'
})
new Vue({
router,
vuetify,
store,
render: (h) => h(App)
}).$mount('#app')
@@ -1,21 +0,0 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '*',
meta: {
title: 'Embed View | Speckle'
},
component: () => import('@/embed/EmbedViewer.vue')
}
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
@@ -1,42 +0,0 @@
import '@mdi/font/css/materialdesignicons.css'
import * as ThemeStateManager from '@/main/utils/themeStateManager'
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
Vue.use(Vuetify)
ThemeStateManager.initialize()
const isDarkMode = ThemeStateManager.isDarkTheme()
export default new Vuetify({
icons: {
iconfont: 'mdi'
},
theme: {
options: { customProperties: true },
dark: isDarkMode,
themes: {
light: {
primary: '#047EFB', //blue
secondary: '#7BBCFF', //light blue
accent: '#FCF25E', //yellow
error: '#FF5555', //red
warning: '#FF9100', //orange
info: '#313BCF', //dark blue
success: '#4caf50',
background: '#eeeeee',
text: '#FFFFFF'
},
dark: {
primary: '#047EFB', //blue
secondary: '#7BBCFF', //light blue
accent: '#FCF25E', //yellow
error: '#FF5555', //red
warning: '#FF9100', //orange
info: '#313BCF', //dark blue
success: '#4caf50',
background: '#3a3b3c'
}
}
}
})
@@ -1,70 +0,0 @@
const SERVER_URL = window.location.origin
// Unauthorised fetch, without token to prevent use of localStorage or exposing elsewhere.
async function speckleFetch(query, variables) {
const res = await fetch(`${SERVER_URL}/graphql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query,
variables
})
})
return await res.json()
}
export const getServerInfo = () => speckleFetch(serverInfoQuery)
export const getStreamObj = (id) => speckleFetch(streamQuery, { id })
export const getBranchObj = (id, branch) => speckleFetch(branchQuery, { id, branch })
export const getCommitObj = (id, commit) => speckleFetch(commitQuery, { id, commit })
const serverInfoQuery = `
query ServerInfo {
serverInfo {
name
}
}
`
const streamQuery = `
query Stream($id: String!) {
stream(id: $id) {
commits(limit: 1) {
totalCount
items {
referencedObject
}
}
}
}
`
const branchQuery = `
query Stream($id: String!, $branch: String!) {
stream(id: $id) {
branch(name: $branch) {
commits(limit: 1) {
totalCount
items {
referencedObject
}
}
}
}
}
`
const commitQuery = `
query Stream($id: String!, $commit: String!) {
stream(id: $id) {
commit(id: $commit) {
referencedObject
}
}
}
`
@@ -358,6 +358,27 @@ export type CommitDeleteInput = {
streamId: Scalars['String'];
};
/** Local-only state used in CommitObjectViewer */
export type CommitObjectViewerState = {
__typename?: 'CommitObjectViewerState';
addingComment: Scalars['Boolean'];
appliedFilter?: Maybe<Scalars['JSONObject']>;
colorLegend: Scalars['JSONObject'];
commentReactions: Array<Scalars['String']>;
emojis: Array<Scalars['String']>;
hideCategoryKey?: Maybe<Scalars['String']>;
hideCategoryValues: Array<Scalars['String']>;
hideKey?: Maybe<Scalars['String']>;
hideValues: Array<Scalars['String']>;
isolateCategoryKey?: Maybe<Scalars['String']>;
isolateCategoryValues: Array<Scalars['String']>;
isolateKey?: Maybe<Scalars['String']>;
isolateValues: Array<Scalars['String']>;
preventCommentCollapse: Scalars['Boolean'];
selectedCommentMetaData?: Maybe<SelectedCommentMetaData>;
viewerBusy: Scalars['Boolean'];
};
export type CommitReceivedInput = {
commitId: Scalars['String'];
message?: InputMaybe<Scalars['String']>;
@@ -814,6 +835,7 @@ export type Query = {
* - get the comments targeting any of a set of provided resources (comments/objects): **pass in an array of resources.**
*/
comments?: Maybe<CommentCollection>;
commitObjectViewerState: CommitObjectViewerState;
serverInfo: ServerInfo;
serverStats: ServerStats;
/**
@@ -958,6 +980,12 @@ export type Scope = {
name: Scalars['String'];
};
export type SelectedCommentMetaData = {
__typename?: 'SelectedCommentMetaData';
id: Scalars['String'];
selectionLocation: Scalars['JSONObject'];
};
export type ServerApp = {
__typename?: 'ServerApp';
author?: Maybe<AppAuthor>;
@@ -1688,6 +1716,21 @@ export type UpdateStreamPermissionMutationVariables = Exact<{
export type UpdateStreamPermissionMutation = { __typename?: 'Mutation', streamUpdatePermission?: boolean | null };
export type StreamFirstCommitQueryVariables = Exact<{
id: Scalars['String'];
}>;
export type StreamFirstCommitQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, referencedObject: string } | null> | null } | null } | null };
export type StreamBranchFirstCommitQueryVariables = Exact<{
id: Scalars['String'];
branch: Scalars['String'];
}>;
export type StreamBranchFirstCommitQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, branch?: { __typename?: 'Branch', commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, referencedObject: string } | null> | null } | null } | null } | null };
export type CommonUserFieldsFragment = { __typename?: 'User', id: string, suuid?: string | null, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, profiles?: Record<string, unknown> | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: any | null } | null> | null } | null };
export type UserFavoriteStreamsQueryVariables = Exact<{
@@ -2211,6 +2254,36 @@ export const UpdateStreamPermission = gql`
streamUpdatePermission(permissionParams: $params)
}
`;
export const StreamFirstCommit = gql`
query StreamFirstCommit($id: String!) {
stream(id: $id) {
id
commits(limit: 1) {
totalCount
items {
id
referencedObject
}
}
}
}
`;
export const StreamBranchFirstCommit = gql`
query StreamBranchFirstCommit($id: String!, $branch: String!) {
stream(id: $id) {
id
branch(name: $branch) {
commits(limit: 1) {
totalCount
items {
id
referencedObject
}
}
}
}
}
`;
export const UserFavoriteStreams = gql`
query UserFavoriteStreams($cursor: String) {
user {
@@ -2418,6 +2491,8 @@ export const StreamWithCollaboratorsDocument = {"kind":"Document","definitions":
export const StreamWithActivityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamWithActivity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"branches"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"activity"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ActivityMainFields"}}]}}]}}]}}]}},...ActivityMainFieldsFragmentDoc.definitions]} as unknown as DocumentNode<StreamWithActivityQuery, StreamWithActivityQueryVariables>;
export const LeaveStreamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"LeaveStream"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamLeave"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}]}]}}]} as unknown as DocumentNode<LeaveStreamMutation, LeaveStreamMutationVariables>;
export const UpdateStreamPermissionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateStreamPermission"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"params"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"StreamUpdatePermissionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamUpdatePermission"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"permissionParams"},"value":{"kind":"Variable","name":{"kind":"Name","value":"params"}}}]}]}}]} as unknown as DocumentNode<UpdateStreamPermissionMutation, UpdateStreamPermissionMutationVariables>;
export const StreamFirstCommitDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamFirstCommit"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}}]}}]}}]}}]}}]} as unknown as DocumentNode<StreamFirstCommitQuery, StreamFirstCommitQueryVariables>;
export const StreamBranchFirstCommitDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamBranchFirstCommit"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"branch"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"branch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"branch"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<StreamBranchFirstCommitQuery, StreamBranchFirstCommitQueryVariables>;
export const UserFavoriteStreamsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserFavoriteStreams"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonUserFields"}},{"kind":"Field","name":{"kind":"Name","value":"favoriteStreams"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonStreamFields"}}]}}]}}]}}]}},...CommonUserFieldsFragmentDoc.definitions,...CommonStreamFieldsFragmentDoc.definitions]} as unknown as DocumentNode<UserFavoriteStreamsQuery, UserFavoriteStreamsQueryVariables>;
export const MainUserDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MainUserData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonUserFields"}}]}}]}},...CommonUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode<MainUserDataQuery, MainUserDataQueryVariables>;
export const ExtraUserDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ExtraUserData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonUserFields"}},{"kind":"Field","name":{"kind":"Name","value":"totalOwnedStreamsFavorites"}}]}}]}},...CommonUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode<ExtraUserDataQuery, ExtraUserDataQueryVariables>;
@@ -0,0 +1,30 @@
extend type Query {
commitObjectViewerState: CommitObjectViewerState!
}
"""
Local-only state used in CommitObjectViewer
"""
type CommitObjectViewerState {
viewerBusy: Boolean!
appliedFilter: JSONObject
isolateKey: String
isolateValues: [String!]!
hideKey: String
hideValues: [String!]!
colorLegend: JSONObject!
isolateCategoryKey: String
isolateCategoryValues: [String!]!
hideCategoryKey: String
hideCategoryValues: [String!]!
selectedCommentMetaData: SelectedCommentMetaData
addingComment: Boolean!
preventCommentCollapse: Boolean!
commentReactions: [String!]!
emojis: [String!]!
}
type SelectedCommentMetaData {
id: String!
selectionLocation: JSONObject!
}
+38
View File
@@ -118,3 +118,41 @@ export const updateStreamPermissionMutation = gql`
streamUpdatePermission(permissionParams: $params)
}
`
/**
* Get a stream's first commit
*/
export const streamFirstCommitQuery = gql`
query StreamFirstCommit($id: String!) {
stream(id: $id) {
id
commits(limit: 1) {
totalCount
items {
id
referencedObject
}
}
}
}
`
/**
* Get a stream branch's first commit
*/
export const streamBranchFirstCommitQuery = gql`
query StreamBranchFirstCommit($id: String!, $branch: String!) {
stream(id: $id) {
id
branch(name: $branch) {
commits(limit: 1) {
totalCount
items {
id
referencedObject
}
}
}
}
}
`
@@ -1,3 +1,4 @@
import { ReactiveVar } from '@apollo/client/core'
import Vue, { VueConstructor } from 'vue'
export type Nullable<T> = T | null
@@ -6,6 +7,13 @@ export type Optional<T> = T | undefined
export type MaybeFalsy<T> = T | null | undefined | false | '' | 0
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type GetReactiveVarType<V extends ReactiveVar<any>> = V extends ReactiveVar<
infer T
>
? T
: unknown
// Copied from Vue typings & improved ergonomics
export type CombinedVueInstance<
Instance extends Vue = Vue,
+3 -4
View File
@@ -2,7 +2,6 @@ import '@/bootstrapper'
import Vue from 'vue'
import App from '@/main/App.vue'
import store from '@/main/store'
import { LocalStorageKeys } from '@/helpers/mainConstants'
import * as MixpanelManager from '@/mixpanelManager'
@@ -30,6 +29,7 @@ import PerfectScrollbar from 'vue2-perfect-scrollbar'
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'
// adds various helper methods
import '@/plugins/helpers'
import { AppLocalStorage } from '@/utils/localStorage'
Vue.use(PerfectScrollbar)
@@ -50,8 +50,8 @@ Vue.filter('capitalize', (value) => {
return value.charAt(0).toUpperCase() + value.slice(1)
})
const AuthToken = localStorage.getItem(LocalStorageKeys.AuthToken)
const RefreshToken = localStorage.getItem(LocalStorageKeys.RefreshToken)
const AuthToken = AppLocalStorage.get(LocalStorageKeys.AuthToken)
const RefreshToken = AppLocalStorage.get(LocalStorageKeys.RefreshToken)
const apolloProvider = createProvider()
installVueApollo(apolloProvider)
@@ -92,7 +92,6 @@ function postAuthInit() {
new Vue({
router,
vuetify,
store,
setup() {
provide(DefaultApolloClient, apolloProvider.defaultClient)
},
@@ -16,6 +16,7 @@
import { signOut } from '@/plugins/authHelpers'
import userQuery from '@/graphql/userById.gql'
import UserAvatarIcon from '@/main/components/common/UserAvatarIcon'
import { AppLocalStorage } from '@/utils/localStorage'
export default {
components: { UserAvatarIcon },
@@ -26,15 +27,15 @@ export default {
},
id: {
type: String,
default: () => localStorage.getItem('uuid')
default: () => AppLocalStorage.get('uuid')
}
},
computed: {
isSelf() {
return this.id === localStorage.getItem('uuid')
return this.id === AppLocalStorage.get('uuid')
},
loggedInUserId() {
return localStorage.getItem('uuid')
return AppLocalStorage.get('uuid')
}
},
apollo: {
@@ -132,6 +132,10 @@ export default {
default: () => {
return { role: null }
}
},
streamId: {
type: String,
required: true
}
},
apollo: {
@@ -147,7 +151,7 @@ export default {
fetchPolicy: 'no-cache',
variables() {
return {
streamId: this.$route.params.streamId,
streamId: this.streamId,
id: this.comment.id
}
},
@@ -169,7 +173,7 @@ export default {
`,
variables() {
return {
streamId: this.$route.params.streamId,
streamId: this.streamId,
commentId: this.comment.id
}
},
@@ -217,7 +221,7 @@ export default {
(r) => r.resourceType !== 'stream'
)
const first = res.shift()
let route = `/streams/${this.$route.params.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.commentDetails.id}`
let route = `/streams/${this.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.commentDetails.id}`
if (res.length !== 0) {
route += `&overlay=${res.map((r) => r.resourceId).join(',')}`
}
@@ -242,7 +246,7 @@ export default {
}
`,
variables: {
streamId: this.$route.params.streamId,
streamId: this.streamId,
commentId: this.comment.id
}
})
@@ -256,7 +260,7 @@ export default {
}
`,
variables: {
streamId: this.$route.params.streamId,
streamId: this.streamId,
commentId: this.comment.id
}
})
@@ -43,6 +43,7 @@ import {
getBlobUrl,
downloadBlobWithUrl
} from '@/main/lib/common/file-upload/blobStorageApi'
import { useCommitObjectViewerParams } from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default Vue.extend({
name: 'CommentThreadAttachmentPreview',
@@ -54,6 +55,10 @@ export default Vue.extend({
},
isOpen: { type: Boolean, required: true }
},
setup() {
const { streamId, resourceId } = useCommitObjectViewerParams()
return { streamId, resourceId }
},
data: () => ({
prettyFileSize,
blobUrl: null,
@@ -83,7 +88,7 @@ export default Vue.extend({
try {
if (this.isImage) {
this.blobUrl = await getBlobUrl(this.attachment.id, {
streamId: this.$route.params.streamId
streamId: this.streamId
})
}
} catch (e) {
@@ -83,6 +83,7 @@ import { gql } from '@apollo/client/core'
import SmartTextEditor from '@/main/components/common/text-editor/SmartTextEditor.vue'
import { SMART_EDITOR_SCHEMA } from '@/main/lib/viewer/comments/commentsHelper'
import CommentThreadReplyAttachments from '@/main/components/comments/CommentThreadReplyAttachments.vue'
import { useCommitObjectViewerParams } from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
components: {
@@ -95,6 +96,10 @@ export default {
stream: { type: Object, default: () => null },
index: { type: Number, default: 0 }
},
setup() {
const { streamId, resourceId, isEmbed } = useCommitObjectViewerParams()
return { streamId, resourceId, isEmbed }
},
data() {
return {
hover: false,
@@ -104,6 +109,7 @@ export default {
},
computed: {
canArchive() {
if (this.isEmbed) return false
if (!this.reply || !this.stream) return false
if (this.stream.role === 'stream:owner' || this.reply.authorId === this.$userId())
return true
@@ -120,7 +126,7 @@ export default {
}
`,
variables: {
streamId: this.$route.params.streamId,
streamId: this.streamId,
commentId: this.reply.id
}
})
@@ -52,103 +52,105 @@
@deleted="handleReplyDeleteEvent"
/>
</template>
<div v-if="$loggedIn()" class="px-0 mb-4">
<v-slide-y-transition>
<template v-if="!isEmbed">
<div v-if="$loggedIn()" class="px-0 mb-4">
<v-slide-y-transition>
<div
v-show="whoIsTyping.length > 0"
class="px-4 py-2 caption mb-2 background rounded-xl"
>
{{ typingStatusText }}
</div>
</v-slide-y-transition>
<div v-if="canReply" class="d-flex mr-2">
<comment-editor
ref="commentEditor"
v-model="replyValue"
:stream-id="streamId"
adding-comment
max-height="300px"
class="mb-2"
:style="{ width: $vuetify.breakpoint.xs ? '100%' : '290px' }"
:disabled="loadingReply"
@input="debTypingUpdate"
@attachments-processing="anyAttachmentsProcessing = $event"
@submit="addReply()"
/>
</div>
<div v-else class="caption background rounded-xl py-2 px-4 mr-4 elevation-2">
You do not have sufficient permissions to reply to comments in this stream.
</div>
<div v-show="loadingReply" class="px-2 mb-2">
<v-progress-linear indeterminate />
</div>
<div
v-show="whoIsTyping.length > 0"
class="px-4 py-2 caption mb-2 background rounded-xl"
v-if="canReply"
ref="replyinput"
class="d-flex justify-space-between align-center comment-actions"
>
{{ typingStatusText }}
<v-btn
v-show="canArchiveThread"
v-tooltip="'Marks this thread as archived.'"
class="white--text ml-2"
small
icon
depressed
color="error"
@click="showArchiveDialog = true"
>
<v-icon small>mdi-delete-outline</v-icon>
</v-btn>
<div class="pr-5">
<v-btn
v-tooltip="'Copy comment url to clipboard'"
:disabled="loadingReply"
class="mouse elevation-5 background mr-3"
icon
large
@click="copyCommentLinkToClip()"
>
<v-icon dark small>mdi-share-variant</v-icon>
</v-btn>
<v-btn
v-tooltip="'Add attachments'"
:disabled="loadingReply"
icon
large
class="mouse elevation-5 background mr-3"
@click="addAttachments()"
>
<v-icon v-if="$vuetify.breakpoint.smAndDown" small>mdi-camera</v-icon>
<v-icon v-else small>mdi-paperclip</v-icon>
</v-btn>
<v-btn
v-tooltip="'Send comment (press enter)'"
:disabled="isSubmitDisabled"
class="mouse elevation-5 primary"
icon
dark
large
@click="addReply()"
>
<v-icon dark small>mdi-send</v-icon>
</v-btn>
</div>
</div>
</v-slide-y-transition>
<div v-if="canReply" class="d-flex mr-2">
<comment-editor
ref="commentEditor"
v-model="replyValue"
:stream-id="$route.params.streamId"
adding-comment
max-height="300px"
class="mb-2"
:style="{ width: $vuetify.breakpoint.xs ? '100%' : '290px' }"
:disabled="loadingReply"
@input="debTypingUpdate"
@attachments-processing="anyAttachmentsProcessing = $event"
@submit="addReply()"
/>
</div>
<div v-else class="caption background rounded-xl py-2 px-4 mr-4 elevation-2">
You do not have sufficient permissions to reply to comments in this stream.
</div>
<div v-show="loadingReply" class="px-2 mb-2">
<v-progress-linear indeterminate />
</div>
<div
v-if="canReply"
ref="replyinput"
class="d-flex justify-space-between align-center comment-actions"
>
<div v-else class="pr-5">
<v-btn
v-show="canArchiveThread"
v-tooltip="'Marks this thread as archived.'"
class="white--text ml-2"
small
icon
block
depressed
color="error"
@click="showArchiveDialog = true"
color="primary"
rounded
class="elevation-5"
large
@click="$loginAndSetRedirect()"
>
<v-icon small>mdi-delete-outline</v-icon>
<v-icon small class="mr-1">mdi-account</v-icon>
Sign in to reply
</v-btn>
<div class="pr-5">
<v-btn
v-tooltip="'Copy comment url to clipboard'"
:disabled="loadingReply"
class="mouse elevation-5 background mr-3"
icon
large
@click="copyCommentLinkToClip()"
>
<v-icon dark small>mdi-share-variant</v-icon>
</v-btn>
<v-btn
v-tooltip="'Add attachments'"
:disabled="loadingReply"
icon
large
class="mouse elevation-5 background mr-3"
@click="addAttachments()"
>
<v-icon v-if="$vuetify.breakpoint.smAndDown" small>mdi-camera</v-icon>
<v-icon v-else small>mdi-paperclip</v-icon>
</v-btn>
<v-btn
v-tooltip="'Send comment (press enter)'"
:disabled="isSubmitDisabled"
class="mouse elevation-5 primary"
icon
dark
large
@click="addReply()"
>
<v-icon dark small>mdi-send</v-icon>
</v-btn>
</div>
</div>
</div>
<div v-else class="pr-5">
<v-btn
block
depressed
color="primary"
rounded
class="elevation-5"
large
@click="$loginAndSetRedirect()"
>
<v-icon small class="mr-1">mdi-account</v-icon>
Sign in to reply
</v-btn>
</div>
</template>
</div>
</div>
<!--
@@ -159,8 +161,10 @@
<div v-else-if="comment.expanded">
<portal to="mobile-comment-thread">
<div
:class="`mobile-thread mouse background ${mobileExpanded ? 'expanded' : ''}`"
style="overflow-y: scroll"
:class="`mobile-thread mouse background ${
mobileExpanded ? 'expanded' : ''
} simple-scrollbar`"
style="overflow-y: auto"
>
<v-card class="elevation-0" style="height: 100vh">
<v-toolbar
@@ -168,6 +172,7 @@
@click.stop="mobileExpanded = !mobileExpanded"
>
<v-btn
v-if="$loggedIn() && canReply && !isEmbed"
v-tooltip="'Add attachments'"
:disabled="loadingReply"
icon
@@ -244,110 +249,112 @@
@deleted="handleReplyDeleteEvent"
/>
</template>
<div v-if="$loggedIn()" class="px-0 mb-4">
<v-slide-y-transition>
<template v-if="!isEmbed">
<div v-if="$loggedIn()" class="px-0 mb-4">
<v-slide-y-transition>
<div
v-show="whoIsTyping.length > 0"
class="px-4 py-2 caption mb-2 background rounded-xl"
>
{{ typingStatusText }}
</div>
</v-slide-y-transition>
<div v-if="canReply" class="d-flex pr-5">
<comment-editor
ref="commentEditor"
v-model="replyValue"
:stream-id="streamId"
:autofocus="false"
adding-comment
max-height="300px"
class="mb-2"
:style="{ width: $vuetify.breakpoint.xs ? '100%' : '290px' }"
:disabled="loadingReply"
@input="debTypingUpdate"
@attachments-processing="anyAttachmentsProcessing = $event"
@submit="addReply()"
/>
</div>
<div
v-show="whoIsTyping.length > 0"
class="px-4 py-2 caption mb-2 background rounded-xl"
v-else
class="caption background rounded-xl py-2 px-4 mr-4 elevation-2"
>
{{ typingStatusText }}
You do not have sufficient permissions to reply to comments in this
stream.
</div>
<div v-show="loadingReply" class="px-2 mb-2">
<v-progress-linear indeterminate />
</div>
<div
v-if="canReply"
ref="replyinput"
class="pb-10 mb-10 d-flex justify-space-between align-center comment-actions"
>
<v-btn
v-show="canArchiveThread"
v-tooltip="'Marks this thread as archived.'"
class="white--text ml-2"
small
icon
depressed
color="error"
@click="showArchiveDialog = true"
>
<v-icon small>mdi-delete-outline</v-icon>
</v-btn>
<div class="pr-5">
<v-btn
v-tooltip="'Copy comment url to clipboard'"
:disabled="loadingReply"
class="mouse elevation-5 background mr-3"
icon
large
@click="copyCommentLinkToClip()"
>
<v-icon dark small>mdi-share-variant</v-icon>
</v-btn>
<v-btn
v-tooltip="'Add attachments'"
:disabled="loadingReply"
icon
large
class="mouse elevation-5 background mr-3"
@click.stop="addAttachments()"
>
<v-icon v-if="$vuetify.breakpoint.smAndDown" small>
mdi-camera-plus
</v-icon>
<v-icon v-else small>mdi-paperclip</v-icon>
</v-btn>
<v-btn
v-tooltip="'Send comment (press enter)'"
:disabled="isSubmitDisabled"
class="mouse elevation-5 primary"
icon
dark
large
@click="addReply()"
>
<v-icon dark small>mdi-send</v-icon>
</v-btn>
</div>
</div>
</v-slide-y-transition>
<div v-if="canReply" class="d-flex pr-5">
<comment-editor
ref="commentEditor"
v-model="replyValue"
:stream-id="$route.params.streamId"
:autofocus="false"
adding-comment
max-height="300px"
class="mb-2"
:style="{ width: $vuetify.breakpoint.xs ? '100%' : '290px' }"
:disabled="loadingReply"
@input="debTypingUpdate"
@attachments-processing="anyAttachmentsProcessing = $event"
@submit="addReply()"
/>
</div>
<div
v-else
class="caption background rounded-xl py-2 px-4 mr-4 elevation-2"
>
You do not have sufficient permissions to reply to comments in this
stream.
</div>
<div v-show="loadingReply" class="px-2 mb-2">
<v-progress-linear indeterminate />
</div>
<div
v-if="canReply"
ref="replyinput"
class="pb-10 mb-10 d-flex justify-space-between align-center comment-actions"
>
<div v-else class="pr-5">
<v-btn
v-show="canArchiveThread"
v-tooltip="'Marks this thread as archived.'"
class="white--text ml-2"
small
icon
block
depressed
color="error"
@click="showArchiveDialog = true"
color="primary"
rounded
class="elevation-5"
large
@click="$loginAndSetRedirect()"
>
<v-icon small>mdi-delete-outline</v-icon>
<v-icon small class="mr-1">mdi-account</v-icon>
Sign in to reply
</v-btn>
<div class="pr-5">
<v-btn
v-tooltip="'Copy comment url to clipboard'"
:disabled="loadingReply"
class="mouse elevation-5 background mr-3"
icon
large
@click="copyCommentLinkToClip()"
>
<v-icon dark small>mdi-share-variant</v-icon>
</v-btn>
<v-btn
v-tooltip="'Add attachments'"
:disabled="loadingReply"
icon
large
class="mouse elevation-5 background mr-3"
@click.stop="addAttachments()"
>
<v-icon v-if="$vuetify.breakpoint.smAndDown" small>
mdi-camera-plus
</v-icon>
<v-icon v-else small>mdi-paperclip</v-icon>
</v-btn>
<v-btn
v-tooltip="'Send comment (press enter)'"
:disabled="isSubmitDisabled"
class="mouse elevation-5 primary"
icon
dark
large
@click="addReply()"
>
<v-icon dark small>mdi-send</v-icon>
</v-btn>
</div>
</div>
</div>
<div v-else class="pr-5">
<v-btn
block
depressed
color="primary"
rounded
class="elevation-5"
large
@click="$loginAndSetRedirect()"
>
<v-icon small class="mr-1">mdi-account</v-icon>
Sign in to reply
</v-btn>
</div>
</template>
</div>
</v-card>
</div>
@@ -385,6 +392,9 @@ import { isDocEmpty } from '@/main/lib/common/text-editor/documentHelper'
import { SMART_EDITOR_SCHEMA } from '@/main/lib/viewer/comments/commentsHelper'
import { isSuccessfullyUploaded } from '@/main/lib/common/file-upload/fileUploadHelper'
import { COMMENT_FULL_INFO_FRAGMENT } from '@/graphql/comments'
import { useCommitObjectViewerParams } from '@/main/lib/viewer/commit-object-viewer/stateManager'
// TODO: The template is a WET mess, need to refactor it
export default {
components: {
@@ -419,7 +429,7 @@ export default {
}
`,
variables() {
return { streamId: this.$route.params.streamId }
return { streamId: this.streamId }
}
},
replyQuery: {
@@ -443,7 +453,7 @@ export default {
fetchPolicy: 'cache-and-network',
variables() {
return {
streamId: this.$route.params.streamId,
streamId: this.streamId,
id: this.comment.id
}
},
@@ -472,7 +482,7 @@ export default {
`,
variables() {
return {
streamId: this.$route.params.streamId,
streamId: this.streamId,
commentId: this.comment.id
}
},
@@ -489,7 +499,7 @@ export default {
}, 100)
}
this.localReplies.push(data.commentThreadActivity.reply)
this.$refs.replyinput.scrollIntoView({ behaviour: 'smooth', block: 'end' })
this.$refs.replyinput?.scrollIntoView({ behaviour: 'smooth', block: 'end' })
return
}
if (data.commentThreadActivity.type === 'comment-archived') {
@@ -517,6 +527,14 @@ export default {
}
}
},
setup() {
const { streamId, resourceId, isEmbed } = useCommitObjectViewerParams()
return {
streamId,
resourceId,
isEmbed
}
},
data() {
return {
hovered: true,
@@ -557,7 +575,7 @@ export default {
return [this.comment, ...sorted]
},
isComplete() {
const res = [this.$route.params.resourceId]
const res = [this.resourceId]
if (this.$route.query.overlay) res.push(...this.$route.query.overlay.split(','))
const commRes = this.comment.resources
.filter((r) => r.resourceType !== 'stream')
@@ -572,7 +590,7 @@ export default {
if (!this.comment) return
const res = this.comment.resources.filter((r) => r.resourceType !== 'stream')
const first = res.shift()
let route = `/streams/${this.$route.params.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.comment.id}`
let route = `/streams/${this.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.comment.id}`
if (res.length !== 0) {
route += `&overlay=${res.map((r) => r.resourceId).join(',')}`
}
@@ -600,7 +618,7 @@ export default {
}
`,
variables: {
streamId: this.$route.params.streamId,
streamId: this.streamId,
commentId: this.comment.id
}
})
@@ -667,7 +685,7 @@ export default {
}
`,
variables: {
sId: this.$route.params.streamId,
sId: this.streamId,
cId: this.comment.id,
d: {
userId: this.$userId(),
@@ -680,7 +698,7 @@ export default {
copyCommentLinkToClip() {
const res = this.comment.resources.filter((r) => r.resourceType !== 'stream')
const first = res.shift()
let route = `${window.origin}/streams/${this.$route.params.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.comment.id}`
let route = `${window.origin}/streams/${this.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.comment.id}`
if (res.length !== 0) {
route += `&overlay=${res.map((r) => r.resourceId).join(',')}`
}
@@ -691,7 +709,7 @@ export default {
})
},
addMissingResources() {
const res = [this.$route.params.resourceId]
const res = [this.resourceId]
if (this.$route.query.overlay) res.push(...this.$route.query.overlay.split(','))
const commRes = this.comment.resources
.filter((r) => r.resourceType !== 'stream')
@@ -725,7 +743,7 @@ export default {
.filter(isSuccessfullyUploaded)
.map((a) => a.result.blobId)
const replyInput = {
streamId: this.$route.params.streamId,
streamId: this.streamId,
parentComment: this.comment.id,
text: this.replyValue.doc,
blobIds
@@ -787,7 +805,7 @@ export default {
}
`,
variables: {
streamId: this.$route.params.streamId,
streamId: this.streamId,
commentId: this.comment.id
}
})
@@ -64,8 +64,8 @@
:key="comment.id + '-card-sidebar'"
no-gutters
:class="`${isUnread(comment) ? 'border' : ''} my-2 property-row rounded-lg ${
$store.state.selectedComment &&
$store.state.selectedComment.id === comment.id
viewerState.selectedCommentMetaData &&
viewerState.selectedCommentMetaData.id === comment.id
? 'elevation-5 selected'
: ''
}`"
@@ -109,10 +109,11 @@
</v-col>
</v-row>
<v-btn
v-if="!isEmbed"
small
block
class="rounded-xl"
:to="`/streams/${$route.params.streamId}/comments`"
:to="`/streams/${streamId}/comments`"
>
all stream comments
</v-btn>
@@ -122,6 +123,10 @@
</template>
<script>
import { documentToBasicString } from '@/main/lib/common/text-editor/documentHelper'
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'
import { computed } from 'vue'
import { useCommitObjectViewerParams } from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
components: {
@@ -137,6 +142,21 @@ export default {
default: 'all'
}
},
setup() {
const { streamId, resourceId, isEmbed } = useCommitObjectViewerParams()
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
selectedCommentMetaData
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return { viewerState, streamId, resourceId, isEmbed }
},
data() {
return {
expand: true,
@@ -94,7 +94,9 @@ export default {
},
computed: {
streamId() {
return this.commit.streamId ?? this.$route.params.streamId
return (
this.commit.streamId ?? this.$route.params.streamId ?? this.$route.query.stream
)
}
}
}
@@ -43,6 +43,8 @@
</div>
</template>
<script>
import { AppLocalStorage } from '@/utils/localStorage'
export default {
props: {
url: {
@@ -168,11 +170,10 @@ export default {
this.imageIndex = index
},
async getPreviewImage(angle = 0) {
const authToken = AppLocalStorage.get('AuthToken')
const res = await fetch(this.url + `/${angle}`, {
signal: this.controller.signal,
headers: localStorage.getItem('AuthToken')
? { Authorization: `Bearer ${localStorage.getItem('AuthToken')}` }
: {}
headers: authToken ? { Authorization: `Bearer ${authToken}` } : {}
})
if (res.headers.has('X-Preview-Error')) {
@@ -14,20 +14,17 @@
</div>
</template>
<script>
import { Viewer, DefaultViewerParams } from '@speckle/viewer'
import throttle from 'lodash/throttle'
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
export default {
name: 'SpeckleViewer',
data() {
return {}
},
watch: {
fullScreen() {
setTimeout(() => {
window.__viewer.onWindowResize()
window.__viewer.cameraHandler.onWindowResize()
}, 20)
setup() {
const { viewer, container, isInitializedPromise } = useInjectedViewer()
return {
viewer,
viewerContainer: container,
isViewerInitializedPromise: isInitializedPromise
}
},
// TODO: pause rendering on destroy, reinit on mounted.
@@ -41,29 +38,26 @@ export default {
// - juggle the container div back in of this component's dom when it's back.
this.$mixpanel.track('Viewer Action', { type: 'action', name: 'load' })
let renderDomElement = document.getElementById('renderer')
if (!renderDomElement) {
renderDomElement = document.createElement('div')
renderDomElement.id = 'renderer'
}
if (!window.__viewer) {
window.__viewer = new Viewer(renderDomElement, DefaultViewerParams)
await window.__viewer.init()
if (!this.viewer || !this.viewerContainer || !this.isViewerInitializedPromise) {
throw new Error('Viewer or its container not properly injected!')
}
this.domElement = renderDomElement
await this.isViewerInitializedPromise
this.domElement = this.viewerContainer
this.domElement.style.display = 'inline-block'
this.$refs.rendererparent.appendChild(renderDomElement)
this.$refs.rendererparent.appendChild(this.domElement)
await window.__viewer.unloadAll()
await this.viewer.unloadAll()
window.__viewer.onWindowResize()
window.__viewer.cameraHandler.onWindowResize()
this.viewer.onWindowResize()
this.viewer.cameraHandler.onWindowResize()
this.setupEvents()
this.$emit('viewer-init')
this.$eventHub.$on('resize-viewer', () => {
window.__viewer.onWindowResize()
window.__viewer.cameraHandler.onWindowResize()
this.viewer.onWindowResize()
this.viewer.cameraHandler.onWindowResize()
})
},
beforeDestroy() {
@@ -73,21 +67,21 @@ export default {
this.domElement.style.display = 'none'
// move renderer dom element outside this component so it doesn't get deleted.
document.body.appendChild(this.domElement)
window.__viewer.unloadAll()
this.viewer.unloadAll()
},
methods: {
setupEvents() {
window.__viewer.on('load-warning', ({ message }) => {
this.viewer.on('load-warning', ({ message }) => {
this.$eventHub.$emit('notification', {
text: message
})
})
window.__viewer.on(
this.viewer.on(
'load-progress',
throttle((args) => this.$emit('load-progress', args), 250)
)
window.__viewer.on('select', (objects) => this.$emit('selection', objects))
this.viewer.on('select', (objects) => this.$emit('selection', objects))
}
}
}
@@ -1,6 +1,6 @@
<template>
<div style="display: inline-block">
<v-menu v-if="$loggedIn()" offset-x open-on-hover :close-on-content-click="false">
<v-menu v-if="isLoggedIn" offset-x open-on-hover :close-on-content-click="false">
<template #activator="{ on, attrs }">
<div v-bind="attrs" v-on="on">
<user-avatar-icon
@@ -72,6 +72,9 @@
<script>
import userByIdQuery from '@/graphql/userById.gql'
import UserAvatarIcon from '@/main/components/common/UserAvatarIcon'
import { AppLocalStorage } from '@/utils/localStorage'
import { LocalStorageKeys } from '@/helpers/mainConstants'
import { useIsLoggedIn } from '@/main/lib/core/composables/auth'
export default {
components: { UserAvatarIcon },
@@ -99,9 +102,13 @@ export default {
default: null
}
},
setup() {
const { isLoggedIn } = useIsLoggedIn()
return { isLoggedIn }
},
computed: {
isSelf() {
return this.id === localStorage.getItem('uuid')
return this.id === AppLocalStorage.get(LocalStorageKeys.Uuid)
}
},
apollo: {
@@ -113,7 +120,7 @@ export default {
}
},
skip() {
return !this.$loggedIn
return !this.isLoggedIn
},
update: (data) => {
return data.user
@@ -0,0 +1,54 @@
<template>
<portal v-if="canRender" :to="to">
<slot />
</portal>
</template>
<script lang="ts">
import { usePortalState } from '@/main/utils/portalStateManager'
import { computed, defineComponent } from 'vue'
/**
* This component should be used instead of <portal> when you have multiple portals that try to use the same <portal-target>,
* possibly at the same time. The priority key will help choose which portal will actually render in the target.
*/
export default defineComponent({
name: 'PrioritizedPortal',
props: {
/**
* Name of portal-target that this portal should reach out to
*/
to: {
type: String,
required: true
},
/**
* Unique identity of this specific portal entrypoint
*/
identity: {
type: String,
required: true
},
/**
* Priority helps figure out which portal entrypoint should take precedence if multiple portals
* attempt to use the same portal-target. A higher number = higher priority.
*/
priority: {
type: Number,
default: 1
}
},
setup(props) {
const { allowedPortals } = usePortalState(
[props.to],
props.identity,
props.priority
)
const canRender = computed(() => allowedPortals.value[props.to])
return {
canRender
}
}
})
</script>
@@ -81,6 +81,7 @@ import ListItemActivity from '@/main/components/activity/ListItemActivity.vue'
import { UserTimelineDocument } from '@/graphql/generated/graphql'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import { AppLocalStorage } from '@/utils/localStorage'
export default {
name: 'FeedTimeline',
@@ -196,7 +197,7 @@ export default {
},
watch: {
timeline(val) {
if (val.totalCount === 0 && !localStorage.getItem('onboarding')) {
if (val.totalCount === 0 && !AppLocalStorage.get('onboarding')) {
this.$router.push('/onboarding')
}
}
@@ -294,6 +294,8 @@
</div>
</template>
<script>
import { AppLocalStorage } from '@/utils/localStorage'
export default {
data: () => ({
length: 5,
@@ -356,11 +358,11 @@ export default {
},
methods: {
skip() {
localStorage.setItem('onboarding', 'skipped')
AppLocalStorage.set('onboarding', 'skipped')
this.$router.push('/')
},
finish() {
localStorage.setItem('onboarding', 'complete')
AppLocalStorage.set('onboarding', 'complete')
this.$router.push('/')
},
prev() {
@@ -63,6 +63,7 @@
</template>
<script>
import { gql } from '@apollo/client/core'
import { AppLocalStorage } from '@/utils/localStorage'
export default {
props: {
@@ -123,7 +124,7 @@ export default {
`/api/stream/${this.$route.params.streamId}/blob/${this.fileId}`,
{
headers: {
Authorization: localStorage.getItem('AuthToken')
Authorization: AppLocalStorage.get('AuthToken')
}
}
)
@@ -40,6 +40,8 @@
</v-card>
</template>
<script>
import { AppLocalStorage } from '@/utils/localStorage'
export default {
props: {
file: {
@@ -71,7 +73,7 @@ export default {
)
request.setRequestHeader(
'Authorization',
`Bearer ${localStorage.getItem('AuthToken')}`
`Bearer ${AppLocalStorage.get('AuthToken')}`
)
request.upload.addEventListener(
@@ -43,8 +43,9 @@
</v-alert>
</transition>
</template>
<script>
import { AppLocalStorage } from '@/utils/localStorage'
export default {
props: {
user: {
@@ -67,7 +68,7 @@ export default {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: localStorage.getItem('AuthToken')
Authorization: AppLocalStorage.get('AuthToken')
},
body: JSON.stringify({ email: this.user.email })
})
@@ -111,6 +111,8 @@
</template>
<script>
import { gql } from '@apollo/client/core'
import { AppLocalStorage } from '@/utils/localStorage'
export default {
components: {
VImageInput: () => import('vuetify-image-input/a-la-carte'),
@@ -133,7 +135,7 @@ export default {
computed: {
isSelf() {
if (!this.user) return false
return this.user.id === localStorage.getItem('uuid')
return this.user.id === AppLocalStorage.get('uuid')
}
},
methods: {
@@ -28,11 +28,18 @@
</v-list>
</v-menu>
</template>
<script>
export default {
<script lang="ts">
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
import { defineComponent } from 'vue'
export default defineComponent({
props: {
small: { type: Boolean, default: false }
},
setup() {
const { viewer } = useInjectedViewer()
return { viewer }
},
data() {
return {
items: [
@@ -45,11 +52,11 @@ export default {
}
},
methods: {
setView(view) {
window.__viewer.interactions.rotateTo(view.toLowerCase())
setView(view: string) {
this.viewer.interactions.rotateTo(view.toLowerCase())
}
}
}
})
</script>
<style scoped>
.test {
@@ -17,7 +17,7 @@
>
<v-slide-x-transition>
<div
v-show="visible && !$store.state.selectedComment"
v-show="visible && !viewerState.selectedCommentMetaData"
ref="commentButton"
class="new-comment-overlay absolute-pos"
>
@@ -44,7 +44,7 @@
<comment-editor
ref="desktopEditor"
v-model="commentValue"
:stream-id="$route.params.streamId"
:stream-id="streamId"
adding-comment
style="width: 300px"
max-height="300px"
@@ -59,7 +59,7 @@
>
<v-fade-transition group>
<template v-if="isCommentEmpty">
<template v-for="reaction in $store.state.commentReactions">
<template v-for="reaction in viewerState.commentReactions">
<v-btn
:key="reaction"
class="mr-2"
@@ -142,7 +142,7 @@
<comment-editor
ref="mobileEditor"
v-model="commentValue"
:stream-id="$route.params.streamId"
:stream-id="streamId"
adding-comment
style="width: 100%"
max-height="60vh"
@@ -169,7 +169,7 @@
</v-btn>
<v-fade-transition group>
<template v-if="isCommentEmpty">
<template v-for="reaction in $store.state.commentReactions">
<template v-for="reaction in viewerState.commentReactions">
<v-btn
:key="reaction"
class="mr-2 elevation-4"
@@ -214,7 +214,7 @@
<portal to="viewercontrols" :order="100">
<v-slide-x-transition>
<v-btn
v-show="!location && !$store.state.selectedComment"
v-show="!location && !viewerState.selectedCommentMetaData"
v-tooltip="'Add a comment (ctrl + shift + c)'"
icon
dark
@@ -233,7 +233,6 @@
import * as THREE from 'three'
import { gql } from '@apollo/client/core'
import { debounce, throttle } from 'lodash'
import { getCamArray } from './viewerFrontendHelpers'
import CommentEditor from '@/main/components/comments/CommentEditor.vue'
import {
basicStringToDocument,
@@ -245,6 +244,14 @@ import {
} from '@/main/lib/viewer/comments/commentsHelper'
import { buildResizeHandlerMixin } from '@/main/lib/common/web-apis/mixins/windowResizeHandler'
import { isSuccessfullyUploaded } from '@/main/lib/common/file-upload/fileUploadHelper'
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
import { getCamArray } from '@/main/lib/viewer/core/helpers/cameraHelper'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import {
setIsAddingComment,
useCommitObjectViewerParams
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
/**
* TODO: Would be nice to get rid of duplicate templates for mobile & large screens
@@ -280,10 +287,28 @@ export default {
}
`,
variables() {
return { streamId: this.$route.params.streamId }
return { streamId: this.streamId }
}
}
},
setup() {
const { streamId, resourceId } = useCommitObjectViewerParams()
const { viewer } = useInjectedViewer()
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
selectedCommentMetaData
commentReactions
appliedFilter
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return { viewer, viewerState, streamId, resourceId }
},
data() {
return {
location: null,
@@ -308,14 +333,14 @@ export default {
},
mounted() {
this.viewerSelectHandler = debounce(this.handleSelect, 10)
window.__viewer.on('select', this.viewerSelectHandler)
this.viewer.on('select', this.viewerSelectHandler)
// Throttling update, cause it happens way too often and triggers expensive DOM updates
// Smoothing out the animation with CSS transitions (check style)
this.viewerControlsUpdateHandler = throttle(() => {
this.updateCommentBubble()
}, VIEWER_UPDATE_THROTTLE_TIME)
window.__viewer.cameraHandler.controls.addEventListener(
this.viewer.cameraHandler.controls.addEventListener(
'update',
this.viewerControlsUpdateHandler
)
@@ -326,8 +351,8 @@ export default {
document.addEventListener('keyup', this.docKeyUpHandler)
},
beforeDestroy() {
window.__viewer.removeListener('select', this.viewerSelectHandler)
window.__viewer.cameraHandler.controls.removeEventListener(
this.viewer.removeListener('select', this.viewerSelectHandler)
this.viewer.cameraHandler.controls.removeEventListener(
'update',
this.viewerControlsUpdateHandler
)
@@ -356,17 +381,17 @@ export default {
this.$mixpanel.track('Comment Action', { type: 'action', name: 'create' })
const camTarget = window.__viewer.cameraHandler.activeCam.controls.getTarget()
const camTarget = this.viewer.cameraHandler.activeCam.controls.getTarget()
const blobIds = this.commentValue.attachments
.filter(isSuccessfullyUploaded)
.map((a) => a.result.blobId)
const commentInput = {
streamId: this.$route.params.streamId,
streamId: this.streamId,
resources: [
{
resourceType: this.$route.path.includes('object') ? 'object' : 'commit',
resourceId: this.$route.params.resourceId
resourceId: this.resourceId
}
],
text: this.commentValue.doc,
@@ -375,12 +400,12 @@ export default {
location: this.location
? this.location
: new THREE.Vector3(camTarget.x, camTarget.y, camTarget.z),
camPos: getCamArray(),
filters: this.$store.state.appliedFilter,
sectionBox: window.__viewer.sectionBox.getCurrentBox(),
camPos: getCamArray(this.viewer),
filters: this.viewerState.appliedFilter,
sectionBox: this.viewer.sectionBox.getCurrentBox(),
selection: null // TODO for later, lazy now
},
screenshot: window.__viewer.interactions.screenshot()
screenshot: this.viewer.interactions.screenshot()
}
if (this.$route.query.overlay) {
commentInput.resources.push(
@@ -419,8 +444,8 @@ export default {
this.expand = false
this.visible = false
this.commentValue = { doc: null, attachments: [] }
this.$store.commit('setAddingCommentState', { addingCommentState: false })
window.__viewer.interactions.deselectObjects()
setIsAddingComment(false)
this.viewer.interactions.deselectObjects()
},
sendStatusUpdate() {
// TODO: typing or not
@@ -436,7 +461,7 @@ export default {
if (!this.location && !this.expand) this.visible = false
this.$store.commit('setAddingCommentState', { addingCommentState: this.expand })
setIsAddingComment(this.expand)
},
handleSelect(info) {
this.expand = false
@@ -444,7 +469,7 @@ export default {
// TODO: deselect event
this.visible = false
this.location = null
this.$store.commit('setAddingCommentState', { addingCommentState: false })
setIsAddingComment(false)
return
}
@@ -462,7 +487,7 @@ export default {
info.location.z
)
const cam = window.__viewer.cameraHandler.camera
const cam = this.viewer.cameraHandler.camera
cam.updateProjectionMatrix()
projectedLocation.project(cam)
let collapsedSize = this.$refs.commentButton.clientWidth
@@ -483,7 +508,7 @@ export default {
// TODO: Clamping, etc.
if (!this.location) return
if (!this.$refs.commentButton) return
const cam = window.__viewer.cameraHandler.camera
const cam = this.viewer.cameraHandler.camera
cam.updateProjectionMatrix()
const projectedLocation = this.location.clone()
projectedLocation.project(cam)
@@ -14,7 +14,7 @@
class="d-flex align-center justify-center no-mouse"
>
<div
v-show="showComments && !$store.state.addingComment"
v-show="showComments && !viewerState.addingComment"
style="
width: 100%;
height: 100vh;
@@ -175,6 +175,16 @@ import { VIEWER_UPDATE_THROTTLE_TIME } from '@/main/lib/viewer/comments/comments
import { buildResizeHandlerMixin } from '@/main/lib/common/web-apis/mixins/windowResizeHandler'
import { documentToBasicString } from '@/main/lib/common/text-editor/documentHelper'
import { COMMENT_FULL_INFO_FRAGMENT } from '@/graphql/comments'
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import {
resetFilter,
setFilterDirectly,
setPreventCommentCollapse,
setSelectedCommentMetaData,
useCommitObjectViewerParams
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
components: {
@@ -204,8 +214,8 @@ export default {
variables() {
const resourceArr = [
{
resourceType: this.$resourceType(this.$route.params.resourceId),
resourceId: this.$route.params.resourceId
resourceType: this.$resourceType(this.resourceId),
resourceId: this.resourceId
}
]
if (this.$route.query.overlay) {
@@ -218,7 +228,7 @@ export default {
}
return {
streamId: this.$route.params.streamId,
streamId: this.streamId,
resources: resourceArr
}
},
@@ -255,11 +265,11 @@ export default {
${COMMENT_FULL_INFO_FRAGMENT}
`,
variables() {
let resIds = [this.$route.params.resourceId]
let resIds = [this.resourceId]
if (this.$route.query.overlay)
resIds = [...resIds, ...this.$route.query.overlay.split(',')]
return {
streamId: this.$route.params.streamId,
streamId: this.streamId,
resourceIds: resIds
}
},
@@ -297,6 +307,25 @@ export default {
}
}
},
setup() {
const { streamId, resourceId } = useCommitObjectViewerParams()
const { viewer } = useInjectedViewer()
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
addingComment
viewerBusy
preventCommentCollapse
emojis
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return { viewer, viewerState, streamId, resourceId }
},
data() {
return {
localComments: [],
@@ -330,7 +359,7 @@ export default {
if (this.$route.query.cId) {
this.openCommentOnInit = this.$route.query.cId
this.commentIntervalChecker = window.setInterval(() => {
if (this.$store.state.viewerBusy || this.$apollo.loading) return
if (this.viewerState.viewerBusy || this.$apollo.loading) return
this.expandComment({ id: this.openCommentOnInit })
this.openCommentOnInit = null
const q = { ...this.$route.query }
@@ -345,15 +374,15 @@ export default {
this.viewerSelectHandler = debounce(() => {
// prevents comment collapse if filters are reset (that triggers a deselect event from the viewer)
if (this.$store.state.preventCommentCollapse) {
this.$store.commit('setPreventCommentCollapse', { value: false })
if (this.viewerState.preventCommentCollapse) {
setPreventCommentCollapse(false)
return
}
for (const c of this.localComments) {
this.collapseComment(c)
}
}, 10)
window.__viewer.on('select', this.viewerSelectHandler)
this.viewer.on('select', this.viewerSelectHandler)
// Throttling update, cause it happens way too often and triggers expensive DOM updates
// Smoothing out the animation with CSS transitions (check style)
@@ -361,7 +390,7 @@ export default {
// console.log('cameraHandler.controls update')
this.updateCommentBubbles()
}, VIEWER_UPDATE_THROTTLE_TIME)
window.__viewer.cameraHandler.controls.addEventListener(
this.viewer.cameraHandler.controls.addEventListener(
'update',
this.viewerControlsUpdateHandler
)
@@ -371,8 +400,8 @@ export default {
}, 1000)
},
beforeDestroy() {
window.__viewer.removeListener('select', this.viewerSelectHandler)
window.__viewer.cameraHandler.controls.removeEventListener(
this.viewer.removeListener('select', this.viewerSelectHandler)
this.viewer.cameraHandler.controls.removeEventListener(
'update',
this.viewerControlsUpdateHandler
)
@@ -384,7 +413,7 @@ export default {
this.updateCommentBubbles()
},
getLeadingEmoji(comment) {
const emojiWhitelist = this.$store.state.emojis
const emojiWhitelist = this.viewerState.emojis
const commentPureText = documentToBasicString(comment.text.doc, 1)
const emojiCandidate = commentPureText.split(' ')[0]
return emojiWhitelist.includes(emojiCandidate) ? emojiCandidate : null
@@ -426,7 +455,7 @@ export default {
for (const c of this.localComments) {
if (c.id === comment.id) {
c.preventAutoClose = true
this.$store.commit('setCommentSelection', { comment: c })
setSelectedCommentMetaData(c)
this.setCommentPow(c)
setTimeout(() => {
c.expanded = true
@@ -448,35 +477,38 @@ export default {
for (const c of this.localComments) {
if (c.id === comment.id && c.expanded) {
c.expanded = false
if (c.data.filters) this.$store.commit('resetFilter')
if (c.data.sectionBox) window.__viewer.sectionBox.off()
this.$store.commit('setCommentSelection', { comment: null })
if (c.data.filters) resetFilter()
if (c.data.sectionBox) this.viewer.sectionBox.off()
setSelectedCommentMetaData(null)
}
}
},
setCommentPow(comment) {
const camToSet = comment.data.camPos
if (camToSet[6] === 1) {
window.__viewer.toggleCameraProjection()
this.viewer.toggleCameraProjection()
}
window.__viewer.interactions.setLookAt(
this.viewer.interactions.setLookAt(
{ x: camToSet[0], y: camToSet[1], z: camToSet[2] }, // position
{ x: camToSet[3], y: camToSet[4], z: camToSet[5] } // target
)
if (camToSet[6] === 1) {
window.__viewer.cameraHandler.activeCam.controls.zoom(camToSet[7], true)
this.viewer.cameraHandler.activeCam.controls.zoom(camToSet[7], true)
}
if (comment.data.filters) {
this.$store.commit('setFilterDirect', { filter: comment.data.filters })
setFilterDirectly({
filter: comment.data.filters
})
} else {
this.$store.commit('resetFilter')
resetFilter()
}
if (comment.data.sectionBox) {
window.__viewer.sectionBox.setBox(comment.data.sectionBox, 0)
window.__viewer.sectionBox.on()
this.viewer.sectionBox.setBox(comment.data.sectionBox, 0)
this.viewer.sectionBox.on()
} else {
window.__viewer.sectionBox.off()
this.viewer.sectionBox.off()
}
},
async handleDeletion(comment) {
@@ -488,7 +520,7 @@ export default {
updateCommentBubbles() {
// console.log('updateCommentBubbles', new Date().toISOString())
if (!this.comments) return
const cam = window.__viewer.cameraHandler.camera
const cam = this.viewer.cameraHandler.camera
cam.updateProjectionMatrix()
for (const comment of this.localComments) {
// get html elements
@@ -1,7 +1,7 @@
<template>
<div @mouseenter="hovered = true" @mouseleave="hovered = false">
<v-card
class="mx-2 my-4 rounded-lg"
class="mx-2 rounded-lg"
:elevation="`${hovered ? 10 : 2}`"
style="transition: all 0.2s ease"
>
@@ -22,7 +22,7 @@
<source-app-avatar :application-name="commit.sourceApplication" />
<v-spacer />
<v-btn
v-if="$route.params.resourceId !== resource.id"
v-if="resourceId !== resource.id"
v-tooltip="'Remove'"
small
icon
@@ -77,6 +77,16 @@
</div>
</template>
<script>
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'
import { computed } from 'vue'
import {
hideObjects,
isolateObjects,
showObjects,
unisolateObjects,
useCommitObjectViewerParams
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
components: {
SourceAppAvatar: () => import('@/main/components/common/SourceAppAvatar'),
@@ -89,6 +99,22 @@ export default {
default: () => null
}
},
setup() {
const { streamId, resourceId } = useCommitObjectViewerParams()
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
isolateValues
hideValues
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return { viewerState, streamId, resourceId }
},
data() {
return {
expanded: false,
@@ -101,14 +127,14 @@ export default {
},
isolated() {
return (
this.$store.state.isolateValues.indexOf(
this.viewerState.isolateValues.indexOf(
this.resource.data.commit.referencedObject
) !== -1
)
},
visible() {
return (
this.$store.state.hideValues.indexOf(
this.viewerState.hideValues.indexOf(
this.resource.data.commit.referencedObject
) === -1
)
@@ -118,12 +144,12 @@ export default {
isolate() {
const id = this.resource.data.commit.referencedObject
if (this.isolated)
this.$store.commit('unisolateObjects', {
unisolateObjects({
filterKey: '__parents',
filterValues: [id]
})
else
this.$store.commit('isolateObjects', {
isolateObjects({
filterKey: '__parents',
filterValues: [id]
})
@@ -131,12 +157,12 @@ export default {
toggleVisibility() {
const id = this.resource.data.commit.referencedObject
if (this.visible)
this.$store.commit('hideObjects', {
hideObjects({
filterKey: '__parents',
filterValues: [id]
})
else
this.$store.commit('showObjects', {
showObjects({
filterKey: '__parents',
filterValues: [id]
})
@@ -0,0 +1,35 @@
<template>
<div>
<slot />
</div>
</template>
<script lang="ts">
import { setupCommitObjectViewer } from '@/main/lib/viewer/commit-object-viewer/stateManager'
import { defineComponent, toRefs } from 'vue'
/**
* Component's only use is to extend injection scope in portals, where it gets lost
* https://portal-vue.linusb.org/guide/caveats.html#local-state-lost-when-toggling-disabled
*/
export default defineComponent({
name: 'CommitObjectViewerScope',
props: {
streamId: {
type: String,
required: true
},
resourceId: {
type: String,
required: true
},
isEmbed: {
type: Boolean,
required: true
}
},
setup(props) {
setupCommitObjectViewer(toRefs(props))
}
})
</script>
@@ -48,7 +48,7 @@
v-if="colorBy"
class="d-inline-block rounded mr-3 mt-1 elevation-3"
:style="`width: 8px; height: 8px; background:${
$store.state.colorLegend[type.fullName]
viewerState.colorLegend[type.fullName]
};`"
></div>
<v-btn
@@ -60,7 +60,7 @@
>
<v-icon class="grey--text" style="font-size: 11px">
{{
$store.state.hideCategoryValues.indexOf(type.fullName) === -1
viewerState.hideCategoryValues.indexOf(type.fullName) === -1
? 'mdi-eye'
: 'mdi-eye-off'
}}
@@ -75,14 +75,14 @@
>
<v-icon
:class="`${
$store.state.isolateCategoryValues.indexOf(type.fullName) !== -1
viewerState.isolateCategoryValues.indexOf(type.fullName) !== -1
? 'primary--text'
: 'grey--text'
}`"
style="font-size: 11px"
>
{{
!$store.state.isolateCategoryValues.indexOf(type.fullName) !== -1
!viewerState.isolateCategoryValues.indexOf(type.fullName) !== -1
? 'mdi-filter'
: 'mdi-filter'
}}
@@ -93,6 +93,15 @@
</div>
</template>
<script>
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'
import { computed } from 'vue'
import {
hideCategoryToggle,
isolateCategoryToggle,
resetFilter,
toggleColorByCategory
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
components: {},
props: {
@@ -101,6 +110,25 @@ export default {
default: () => null
}
},
setup() {
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
colorLegend
hideCategoryValues
isolateCategoryValues
appliedFilter
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return {
viewerState
}
},
data() {
return {
hidden: [],
@@ -112,7 +140,7 @@ export default {
},
computed: {
colorBy() {
return this.$store.state.appliedFilter && this.$store.state.appliedFilter.colorBy
return this.viewerState.appliedFilter?.colorBy
}
},
watch: {
@@ -124,7 +152,7 @@ export default {
this.generateTypeMap(this.filter)
},
beforeDestroy() {
this.$store.commit('resetFilter')
resetFilter()
},
methods: {
mashColorLegend(colorLegend) {
@@ -136,10 +164,10 @@ export default {
}
},
async toggleColors() {
this.$store.commit('toggleColorByCategory', { filterKey: this.filter.targetKey })
toggleColorByCategory({ filterKey: this.filter.targetKey })
},
async toggleFilter(type) {
this.$store.commit('isolateCategoryToggle', {
isolateCategoryToggle({
colorBy: this.colorBy,
filterKey: this.filter.targetKey,
filterValue: type,
@@ -147,7 +175,7 @@ export default {
})
},
async toggleVisibility(type) {
this.$store.commit('hideCategoryToggle', {
hideCategoryToggle({
colorBy: this.colorBy,
filterKey: this.filter.targetKey,
filterValue: type
@@ -79,7 +79,23 @@
</div>
</template>
<script>
import {
resetFilter,
setNumericFilter
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
components: {
HistogramSlider: async () => {
await import(
/* webpackChunkName: "vue-histogram-slider" */ 'vue-histogram-slider/dist/histogram-slider.css'
)
const component = await import(
/* webpackChunkName: "vue-histogram-slider" */ 'vue-histogram-slider'
)
return component
}
},
props: {
filter: {
type: Object,
@@ -109,13 +125,13 @@ export default {
this.$set(this.range, 0, this.filter.data.minValue)
this.$set(this.range, 1, this.filter.data.maxValue)
this.setFilter()
this.width = this.$refs.parent.clientWidth - 24
this.width = this.$refs.parent ? this.$refs.parent.clientWidth - 24 : 300
this.$eventHub.$on('resize-viewer', () => {
this.width = this.$refs.parent?.clientWidth - 24
this.width = this.$refs.parent ? this.$refs.parent.clientWidth - 24 : 300
})
},
beforeDestroy() {
this.$store.commit('resetFilter')
resetFilter()
},
methods: {
async setFilterHistogram(e) {
@@ -123,7 +139,8 @@ export default {
this.preventFirstSetInternal = false
return
}
this.$store.commit('setNumericFilter', {
setNumericFilter({
filterKey: this.filter.targetKey,
minValue: e.from,
maxValue: e.to
@@ -134,7 +151,8 @@ export default {
this.preventFirstSetInternal = false
return
}
this.$store.commit('setNumericFilter', {
setNumericFilter({
filterKey: this.filter.targetKey,
minValue: this.range[0],
maxValue: this.range[1]
@@ -1,6 +1,6 @@
<template>
<div>
<v-card class="mx-2 mb-2 rounded-lg">
<v-card class="mx-2 rounded-lg">
<v-toolbar
v-ripple
class="transparent"
@@ -14,7 +14,7 @@
<v-chip small class="ma-1 caption grey white--text no-hover">Object</v-chip>
<v-spacer />
<v-btn
v-if="$route.params.resourceId !== resource.id"
v-if="resourceId !== resource.id"
v-tooltip="'Remove'"
small
icon
@@ -55,6 +55,16 @@
</div>
</template>
<script>
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import gql from 'graphql-tag'
import {
hideObjects,
isolateObjects,
showObjects,
unisolateObjects,
useCommitObjectViewerParams
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
components: {
ObjectProperties: () => import('@/main/components/viewer/ObjectProperties')
@@ -65,6 +75,22 @@ export default {
default: () => null
}
},
setup() {
const { streamId, resourceId } = useCommitObjectViewerParams()
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
isolateValues
hideValues
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return { viewerState, streamId, resourceId }
},
data() {
return {
expanded: false
@@ -72,24 +98,22 @@ export default {
},
computed: {
isolated() {
return (
this.$store.state.isolateValues.indexOf(this.resource.data.object.id) !== -1
)
return this.viewerState.isolateValues.indexOf(this.resource.data.object.id) !== -1
},
visible() {
return this.$store.state.hideValues.indexOf(this.resource.data.object.id) === -1
return this.viewerState.hideValues.indexOf(this.resource.data.object.id) === -1
}
},
methods: {
isolate() {
const id = this.resource.data.object.id
if (this.isolated)
this.$store.commit('unisolateObjects', {
unisolateObjects({
filterKey: '__parents',
filterValues: [id]
})
else
this.$store.commit('isolateObjects', {
isolateObjects({
filterKey: '__parents',
filterValues: [id]
})
@@ -97,12 +121,12 @@ export default {
toggleVisibility() {
const id = this.resource.data.object.id
if (this.visible)
this.$store.commit('hideObjects', {
hideObjects({
filterKey: '__parents',
filterValues: [id]
})
else
this.$store.commit('showObjects', {
showObjects({
filterKey: '__parents',
filterValues: [id]
})
@@ -124,6 +124,15 @@
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import gql from 'graphql-tag'
import {
hideObjects,
isolateObjects,
showObjects,
unisolateObjects
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
components: {
@@ -147,6 +156,21 @@ export default {
default: () => null
}
},
setup() {
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
isolateValues
hideValues
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return { viewerState }
},
data() {
return {
expanded: false,
@@ -168,11 +192,11 @@ export default {
},
visible() {
if (this.prop.type === 'object') {
return this.$store.state.hideValues.indexOf(this.prop.value.referencedId) === -1
return this.viewerState.hideValues.indexOf(this.prop.value.referencedId) === -1
}
if (this.prop.type === 'array') {
const ids = this.prop.value.map((o) => o.referencedId)
const targetIds = this.$store.state.hideValues.filter(
const targetIds = this.viewerState.hideValues.filter(
(val) => ids.indexOf(val) !== -1
)
if (targetIds.length === 0) return true
@@ -183,12 +207,12 @@ export default {
isolated() {
if (this.prop.type === 'object') {
return (
this.$store.state.isolateValues.indexOf(this.prop.value.referencedId) !== -1
this.viewerState.isolateValues.indexOf(this.prop.value.referencedId) !== -1
)
}
if (this.prop.type === 'array') {
const ids = this.prop.value.map((o) => o.referencedId)
const targetIds = this.$store.state.isolateValues.filter(
const targetIds = this.viewerState.isolateValues.filter(
(val) => ids.indexOf(val) !== -1
)
if (targetIds.length === 0) return false
@@ -207,12 +231,12 @@ export default {
}
if (this.visible)
this.$store.commit('hideObjects', {
hideObjects({
filterKey: '__parents',
filterValues: targetIds
})
else
this.$store.commit('showObjects', {
showObjects({
filterKey: '__parents',
filterValues: targetIds
})
@@ -225,12 +249,12 @@ export default {
}
if (this.isolated)
this.$store.commit('unisolateObjects', {
unisolateObjects({
filterKey: '__parents',
filterValues: targetIds
})
else
this.$store.commit('isolateObjects', {
isolateObjects({
filterKey: '__parents',
filterValues: targetIds
})
@@ -64,6 +64,13 @@
</div>
</template>
<script>
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import gql from 'graphql-tag'
import {
isolateObjects,
unisolateObjects
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
components: {
ObjectPropertiesRow: () => import('@/main/components/viewer/ObjectPropertiesRow')
@@ -78,6 +85,20 @@ export default {
default: null
}
},
setup() {
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
isolateValues
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return { viewerState }
},
data() {
return {
expand: true
@@ -101,7 +122,7 @@ export default {
isolated() {
const ids = this.objects.map((o) => o.id)
ids.forEach((val) => {
if (this.$store.state.isolateValues.indexOf(val) === -1) return false
if (this.viewerState.isolateValues.indexOf(val) === -1) return false
})
return true
}
@@ -110,12 +131,12 @@ export default {
isolateSelection() {
const ids = this.objects.map((o) => o.id)
if (!this.isolated)
this.$store.commit('unisolateObjects', {
unisolateObjects({
filterKey: '__parents',
filterValues: ids
})
else
this.$store.commit('isolateObjects', {
isolateObjects({
filterKey: '__parents',
filterValues: ids
})
@@ -5,6 +5,7 @@
<div v-for="(resource, index) in resources" :key="index">
<commit-info-resource
v-if="resource.type === 'commit'"
:class="[index === 0 ? 'my-2' : 'my-4']"
:resource="resource"
@remove="
(e) => {
@@ -15,6 +16,7 @@
></commit-info-resource>
<object-info-resource
v-if="resource.type === 'object'"
:class="[index === 0 ? 'my-2' : 'my-4']"
:resource="resource"
@remove="
(e) => {
@@ -24,7 +26,7 @@
"
></object-info-resource>
</div>
<div v-show="$loggedIn()" class="px-2 mb-2">
<div v-if="allowAdd && isLoggedIn" class="px-2 mb-2">
<v-btn
v-tooltip="'Overlay another commit or object'"
block
@@ -71,13 +73,22 @@
</div>
</template>
<script>
import { useIsLoggedIn } from '@/main/lib/core/composables/auth'
export default {
components: {
CommitInfoResource: () => import('@/main/components/viewer/CommitInfoResource'),
ObjectInfoResource: () => import('@/main/components/viewer/ObjectInfoResource')
},
props: {
resources: { type: Array, default: () => [] }
resources: { type: Array, default: () => [] },
allowAdd: {
type: Boolean,
default: true
}
},
setup() {
const { isLoggedIn } = useIsLoggedIn()
return { isLoggedIn }
},
data() {
return {
@@ -2,7 +2,7 @@
<div
ref="parent"
:style="`width: 100%; height: 100vh; position: absolute; pointer-events: none; overflow: hidden; opacity: ${
$store.state.selectedComment || $store.state.addingComment ? '0.2' : '1'
viewerState.selectedCommentMetaData || viewerState.addingComment ? '0.2' : '1'
};`"
>
<div v-show="showBubbles">
@@ -91,6 +91,14 @@ import * as THREE from 'three'
import { gql } from '@apollo/client/core'
import { v4 as uuid } from 'uuid'
import debounce from 'lodash/debounce'
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import {
resetFilter,
setFilterDirectly,
useCommitObjectViewerParams
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
name: 'ViewerBubbles',
@@ -121,12 +129,12 @@ export default {
`,
variables() {
return {
streamId: this.$route.params.streamId,
resourceId: this.$route.params.resourceId
streamId: this.streamId,
resourceId: this.resourceId
}
},
skip() {
return !this.$route.params.resourceId || !this.$loggedIn()
return !this.resourceId || !this.$loggedIn()
},
result(res) {
const data = res.data
@@ -177,6 +185,24 @@ export default {
}
}
},
setup() {
const { streamId, resourceId } = useCommitObjectViewerParams()
const { viewer } = useInjectedViewer()
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
selectedCommentMetaData
addingComment
appliedFilter
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return { viewer, viewerState, streamId, resourceId }
},
data() {
return {
uuid: uuid(),
@@ -198,7 +224,7 @@ export default {
this.uuid = window.__bubblesId
this.raycaster = new THREE.Raycaster()
window.__viewer.cameraHandler.controls.addEventListener('update', () =>
this.viewer.cameraHandler.controls.addEventListener('update', () =>
this.updateBubbles(false)
)
@@ -206,9 +232,7 @@ export default {
window.addEventListener('beforeunload', async () => {
await this.sendDisconnect()
})
this.resourceId = this.$route.params.resourceId
window.__resourceId = this.$route.params.resourceId
window.__viewer.on(
this.viewer.on(
'select',
debounce((selectionInfo) => {
this.selectedIds = selectionInfo.userData.map((o) => o.id)
@@ -217,7 +241,7 @@ export default {
this.sendUpdateAndPrune()
}, 50)
)
window.__viewer.on('object-doubleclicked', () => {})
this.viewer.on('object-doubleclicked', () => {})
},
async beforeDestroy() {
await this.sendDisconnect()
@@ -227,26 +251,27 @@ export default {
setUserPow(user) {
const camToSet = user.camera
if (camToSet[6] === 1) {
window.__viewer.toggleCameraProjection()
this.viewer.toggleCameraProjection()
}
window.__viewer.interactions.setLookAt(
this.viewer.interactions.setLookAt(
{ x: camToSet[0], y: camToSet[1], z: camToSet[2] }, // position
{ x: camToSet[3], y: camToSet[4], z: camToSet[5] } // target
)
if (camToSet[6] === 1) {
window.__viewer.cameraHandler.activeCam.controls.zoom(camToSet[7], true)
this.viewer.cameraHandler.activeCam.controls.zoom(camToSet[7], true)
}
if (user.filter) this.$store.commit('setFilterDirect', { filter: user.filter })
else this.$store.commit('resetFilter')
if (user.filter) setFilterDirectly({ filter: user.filter })
else resetFilter()
if (user.sectionBox) {
window.__viewer.sectionBox.on()
window.__viewer.sectionBox.setBox(user.sectionBox, 0)
this.viewer.sectionBox.on()
this.viewer.sectionBox.setBox(user.sectionBox, 0)
}
this.$mixpanel.track('Bubbles Action', { type: 'action', name: 'avatar-click' })
},
async sendUpdateAndPrune() {
if (!this.$route.params.resourceId) return
if (!this.resourceId) return
for (const user of this.users) {
const delta = Date.now() - user.lastUpdate
if (delta > 20000) {
@@ -262,7 +287,7 @@ export default {
if (!this.$loggedIn()) return
const controls = window.__viewer.cameraHandler.activeCam.controls
const controls = this.viewer.cameraHandler.activeCam.controls
const pos = controls.getPosition()
const target = controls.getTarget()
const c = [
@@ -272,20 +297,20 @@ export default {
parseFloat(target.x.toFixed(5)),
parseFloat(target.y.toFixed(5)),
parseFloat(target.z.toFixed(5)),
window.__viewer.cameraHandler.activeCam.name === 'ortho' ? 1 : 0,
this.viewer.cameraHandler.activeCam.name === 'ortho' ? 1 : 0,
controls._zoom
]
let selectionLocation = this.selectionLocation
if (this.$store.state.selectedComment) {
selectionLocation = this.$store.state.selectedComment.data.location
if (this.viewerState.selectedCommentMetaData) {
selectionLocation = this.viewerState.selectedCommentMetaData.selectionLocation
}
const data = {
filter: this.$store.state.appliedFilter,
filter: this.viewerState.appliedFilter,
selection: this.selectedIds,
selectionLocation,
sectionBox: window.__viewer.sectionBox.getCurrentBox(),
sectionBox: this.viewer.sectionBox.getCurrentBox(),
selectionCenter: this.selectionCenter,
camera: c,
userId: this.$userId(),
@@ -294,7 +319,7 @@ export default {
status: 'viewing'
}
if (!this.$route.params.streamId) return
if (!this.streamId) return
await this.$apollo.mutate({
mutation: gql`
mutation userViewerActivityBroadcast(
@@ -310,15 +335,15 @@ export default {
}
`,
variables: {
streamId: this.$route.params.streamId,
resourceId: this.$route.params.resourceId,
streamId: this.streamId,
resourceId: this.resourceId,
data
}
})
},
async sendDisconnect() {
if (!this.$loggedIn()) return
if (!this.$route.params.streamId) return
if (!this.streamId) return
await this.$apollo.mutate({
mutation: gql`
@@ -335,17 +360,16 @@ export default {
}
`,
variables: {
streamId: this.$route.params.streamId,
resourceId: this.resourceId || window.__resourceId,
streamId: this.streamId,
resourceId: this.resourceId,
data: { userId: this.$userId(), uuid: this.uuid, status: 'disconnect' }
}
})
delete window.__resourceId
},
updateBubbles(transition = true) {
if (!this.$refs.parent) return
const cam = window.__viewer.cameraHandler.camera
const cam = this.viewer.cameraHandler.camera
cam.updateProjectionMatrix()
const selectedObjects = []
for (const user of this.users) {
@@ -442,7 +466,7 @@ export default {
uArrowEl.style.opacity = user.clipped ? '0' : '1'
}
window.__viewer.interactions.overlayObjects(selectedObjects)
this.viewer.interactions.overlayObjects(selectedObjects)
}
}
}
@@ -58,6 +58,11 @@
</div>
</template>
<script>
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import gql from 'graphql-tag'
import { resetFilter } from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
components: {
CanonicalViews: () => import('@/main/components/viewer/CanonicalViews')
@@ -65,6 +70,21 @@ export default {
props: {
small: { type: Boolean, default: false }
},
setup() {
const { viewer } = useInjectedViewer()
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
appliedFilter
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return { viewer, viewerState }
},
data() {
return {
fullScreen: false
@@ -72,7 +92,7 @@ export default {
},
computed: {
showVisReset() {
return !!this.$store.state.appliedFilter
return !!this.viewerState.appliedFilter
}
},
mounted() {
@@ -80,16 +100,16 @@ export default {
},
methods: {
toggleCamera() {
window.__viewer.toggleCameraProjection()
this.viewer.toggleCameraProjection()
},
resetVisibility() {
this.$store.commit('resetFilter')
resetFilter()
},
zoomEx() {
window.__viewer.zoomExtents()
this.viewer.zoomExtents()
},
sectionToggle() {
window.__viewer.toggleSectionBox()
this.viewer.toggleSectionBox()
}
}
}
@@ -86,6 +86,9 @@
</v-list>
</template>
<script>
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'
import { computed } from 'vue'
export default {
name: 'ViewerFilters',
components: {
@@ -107,6 +110,20 @@ export default {
default: true
}
},
setup() {
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
appliedFilter
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return { viewerState }
},
data() {
return {
expand: false,
@@ -149,10 +166,10 @@ export default {
this.parseAndSetFilters()
}
},
'$store.state.appliedFilter'() {
'viewerState.appliedFilter'() {
if (this.trySetPresetFilter) return
if (this.$store.state.appliedFilter && this.$store.state.appliedFilter.filterBy) {
const key = Object.keys(this.$store.state.appliedFilter.filterBy)[0]
if (this.viewerState.appliedFilter?.filterBy) {
const key = Object.keys(this.viewerState.appliedFilter.filterBy)[0]
const presetFilter = this.allFilters.find((f) => f.targetKey === key)
if (presetFilter) this.activeFilter = presetFilter
this.trySetPresetFilter = true
@@ -33,6 +33,7 @@
</v-list>
</template>
<script>
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
export default {
props: {
views: {
@@ -44,6 +45,10 @@ export default {
default: true
}
},
setup() {
const { viewer } = useInjectedViewer()
return { viewer }
},
data() {
return {
expand: false
@@ -51,7 +56,7 @@ export default {
},
methods: {
setView(view) {
window.__viewer.interactions.setView(view.id)
this.viewer.interactions.setView(view.id)
}
}
}
@@ -100,7 +100,6 @@ export default {
default: () => null
}
},
apollo: {},
data() {
return {
items: ['All Commits'],
@@ -158,7 +157,7 @@ export default {
}
if (pcs.length !== 1) {
const streamId = pcs[2]
if (streamId !== this.$route.params.streamId) {
if (streamId !== this.streamId) {
this.objectIdError = 'Objects do not belong to the same stream.'
this.objectId = null
return
@@ -0,0 +1,143 @@
<template>
<div class="embed-viewer-core">
<!-- Top bar (menu toggle + powered by speckle text) -->
<div
class="embed-viewer-core__top-bar top-left bottom-left pa-4 d-flex justify-space-between"
style="right: 0px; position: fixed; z-index: 5; width: 100%"
>
<v-btn fab small style="z-index=1000" @click="drawer = !drawer">
<v-icon>mdi-menu</v-icon>
</v-btn>
<span v-show="!drawer" class="caption d-inline-flex align-center">
<img src="@/assets/logo.svg" height="18" />
<span style="margin-top: 2px" class="primary--text">
<a href="https://speckle.xyz" target="_blank" class="text-decoration-none">
<b>Powered by Speckle</b>
</a>
</span>
</span>
</div>
<!-- Viewer filters panel / sidebar -->
<v-navigation-drawer
ref="drawerRef"
v-model="drawer"
class="viewer-controls-drawer"
app
floating
:class="`grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'} elevation-1`"
:width="navWidth"
disable-resize-watcher
style="z-index: 100"
>
<div class="px-1 pt-1 d-flex flex-column" style="height: 100%; width: 100%">
<!-- Drawer closer -->
<v-btn icon small class="align-self-end mb-2" @click="drawer = false">
<v-icon x-small>mdi-close</v-icon>
</v-btn>
<!-- Sidebar portal -->
<portal-target name="nav" />
</div>
</v-navigation-drawer>
<!-- Actual viewer -->
<div class="embed-viewer-core__viewer viewer-wrapper no-scrollbar">
<commit-object-viewer
:stream-id="streamId"
:resource-id="resourceId"
:is-embed="true"
@models-loaded="onModelsLoaded"
/>
</div>
<!-- Appending buttons to viewercontrols (these should be ordered last) -->
<portal to="viewercontrols" :order="100">
<v-btn
v-tooltip="'View extra details in Speckle!'"
icon
dark
large
class="elevation-5 primary pa-0 ma-o"
:href="goToServerUrl"
target="blank"
>
<v-icon dark small>mdi-open-in-new</v-icon>
</v-btn>
</portal>
</div>
</template>
<script lang="ts">
import Vue, { computed, defineComponent, Ref, ref } from 'vue'
import CommitObjectViewer from '@/main/pages/stream/CommitObjectViewer.vue'
import { useEmbedViewerQuery } from '@/main/lib/viewer/commit-object-viewer/composables/embed'
import { useNavigationDrawerAutoResize } from '@/main/lib/core/composables/dom'
import { Nullable } from '@/helpers/typeHelpers'
export default defineComponent({
name: 'EmbeddedCommitObjectViewer',
components: {
CommitObjectViewer
},
props: {
streamId: {
type: String,
required: true
},
resourceId: {
type: String,
required: true
}
},
setup(_, { emit }) {
const drawerRef: Ref<Nullable<Vue>> = ref(null)
const loadedModel = ref(false)
const drawer = ref(false)
const { navWidth } = useNavigationDrawerAutoResize({
drawerRef
})
const { streamId, commitId, objectId, branchName } = useEmbedViewerQuery()
const goToServerUrl = computed(() => {
const base = `${window.location.origin}/streams/${streamId.value}/`
if (commitId.value) return base + `commits/${commitId.value}`
if (objectId.value) return base + `objects/${objectId.value}`
if (branchName.value) return base + `branches/${encodeURI(branchName.value)}`
return base
})
return {
goToServerUrl,
loadedModel,
drawer,
// drawer ref must be returned, for it to be filled
drawerRef,
navWidth,
onModelsLoaded: () => {
loadedModel.value = true
emit('models-loaded')
}
}
}
})
</script>
<style lang="scss" scoped>
.embed-viewer-core {
position: relative;
width: 100%;
height: 100%;
&__viewer {
height: 100vh !important;
width: 100vw !important;
&::-webkit-scrollbar {
display: none;
}
top: 0;
left: 0;
}
}
</style>
@@ -1,16 +0,0 @@
export function getCamArray() {
const controls = window.__viewer.cameraHandler.activeCam.controls
const pos = controls.getPosition()
const target = controls.getTarget()
const c = [
parseFloat(pos.x.toFixed(5)),
parseFloat(pos.y.toFixed(5)),
parseFloat(pos.z.toFixed(5)),
parseFloat(target.x.toFixed(5)),
parseFloat(target.y.toFixed(5)),
parseFloat(target.z.toFixed(5)),
window.__viewer.cameraHandler.activeCam.name === 'ortho' ? 1 : 0,
controls._zoom
]
return c
}
@@ -114,6 +114,7 @@
<script>
import { gql } from '@apollo/client/core'
import { userSearchQuery } from '@/graphql/user'
import { AppLocalStorage } from '@/utils/localStorage'
export default {
components: {
@@ -180,7 +181,7 @@ export default {
},
methods: {
addCollab(user) {
if (user.id === localStorage.getItem('uuid')) return
if (user.id === AppLocalStorage.get('uuid')) return
const indx = this.collabs.findIndex((u) => u.id === user.id)
if (indx !== -1) return
@@ -0,0 +1,25 @@
<template>
<v-app id="speckle">
<v-main class="background">
<router-view></router-view>
</v-main>
<global-toast />
<global-loading />
</v-app>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
/**
* A very basic layout that does the core required setup, but doesn't add any navbars, menus or anything. Can be used
* for embed or other simplified pages.
*/
export default defineComponent({
name: 'TheBasic',
components: {
GlobalToast: () => import('@/main/components/common/GlobalToast.vue'),
GlobalLoading: () => import('@/main/components/common/GlobalLoading.vue')
}
})
</script>
+16 -81
View File
@@ -1,7 +1,7 @@
<template>
<v-app id="speckle">
<v-navigation-drawer
ref="drawer"
ref="navDrawer"
v-model="drawer"
app
floating
@@ -63,7 +63,8 @@
<script>
import { gql } from '@apollo/client/core'
import { mainUserDataQuery } from '@/graphql/user'
import { setDarkTheme } from '@/main/utils/themeStateManager'
import { useNavigationDrawerAutoResize } from '../lib/core/composables/dom'
import { ref } from 'vue'
export default {
name: 'TheMain',
@@ -107,33 +108,35 @@ export default {
}
}
},
setup() {
const navDrawer = ref(null)
const { navWidth } = useNavigationDrawerAutoResize({
drawerRef: navDrawer
})
// drawer ref must be returned, for it to be filled
return {
navDrawer,
navWidth
}
},
data() {
return {
newStreamDialog: 1,
drawer: true,
navWidth: 300,
navRestWidth: 300,
borderSize: 3,
hideEmailBanner: false
}
},
watch: {
$route: {
handler(to) {
if (!to.meta.resizableNavbar) {
this.navWidth = this.navRestWidth
}
if (to.meta.resizableNavbar && window.__lastNavSize) {
this.navWidth = window.__lastNavSize
}
this.hideEmailBanner = !!to.meta.hideEmailBanner
},
immediate: true
}
},
mounted() {
this.setNavResizeEvents()
if (this.$route.query.emailverfiedstatus) {
setTimeout(() => {
this.$eventHub.$emit('notification', {
@@ -141,78 +144,10 @@ export default {
})
}, 1000) // todo: ask fabian if there's a better way, feels icky
}
},
methods: {
switchTheme() {
this.$vuetify.theme.dark = !this.$vuetify.theme.dark
setDarkTheme(this.$vuetify.theme.dark, true)
this.$mixpanel.people.set(
'Theme Web',
this.$vuetify.theme.dark ? 'dark' : 'light'
)
},
setNavResizeEvents() {
const minSize = this.borderSize
const el = this.$refs.drawer.$el
const drawerBorder = el.querySelector('.nav-resizer')
drawerBorder.style.cursor = 'ew-resize'
function resize(e) {
e.preventDefault()
const maxWidth = document.body.offsetWidth / 2
const minWidth = 300
document.body.style.cursor = 'ew-resize'
if (!(e.clientX > maxWidth || e.clientX < minWidth)) {
el.style.width = e.clientX + 'px'
window.__lastNavSize = e.clientX
}
}
drawerBorder.addEventListener(
'mousedown',
(e) => {
e.preventDefault()
if (e.offsetX < minSize) {
el.style.transition = 'initial'
document.addEventListener('mousemove', resize, false)
}
},
false
)
document.addEventListener(
'mouseup',
(e) => {
e.preventDefault()
el.style.transition = ''
document.body.style.cursor = ''
this.navWidth = el.style.width
document.removeEventListener('mousemove', resize, false)
setTimeout(() => this.$eventHub.$emit('resize-viewer'), 300)
},
false
)
}
}
}
</script>
<style scoped>
.nav-resizer {
position: absolute;
top: 0;
right: 0;
width: 0px;
height: 100%;
z-index: 100000;
transition: all 0.6s ease;
opacity: 0.01;
border: 4px solid royalblue;
}
.nav-resizer:hover {
opacity: 0.5;
width: 0px;
}
.email-banner {
z-index: 2;
}
@@ -0,0 +1,12 @@
import { IsLoggedInDocument } from '@/graphql/generated/graphql'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
/**
* Composable that resolves whether the user is logged in through an Apollo query
*/
export function useIsLoggedIn() {
const { result } = useQuery(IsLoggedInDocument)
const isLoggedIn = computed(() => !!result.value?.user?.id)
return { isLoggedIn }
}
@@ -0,0 +1,12 @@
import { ComposableInvokedOutOfScopeError } from '@/main/lib/core/errors/composition'
import { getCurrentInstance } from 'vue'
/**
* Get EventHub
*/
export function useEventHub() {
const vm = getCurrentInstance()
if (!vm) throw new ComposableInvokedOutOfScopeError()
return vm.proxy.$eventHub
}
@@ -0,0 +1,106 @@
import { useEventHub } from '@/main/lib/core/composables/core'
import { useRoute } from '@/main/lib/core/composables/router'
import { onBeforeUnmount, onMounted, Ref, ref, unref, watch } from 'vue'
/**
* Sets up v-navigation-drawer autoresize (ask @dim how this works)
*/
export function useNavigationDrawerAutoResize(params: {
navRestWidth?: number
borderSize?: number
drawerRef: Ref<Vue | null> | Vue
}) {
const { navRestWidth = 300, borderSize = 3, drawerRef } = params
const navWidth = ref<string | number>(navRestWidth)
const minSize = borderSize
const route = useRoute()
const eventHub = useEventHub()
const drawerEl = () => {
const comp = unref(drawerRef)
if (!comp)
throw new Error(
"Couldn't resolve drawer ref! Is it returned from the setup function?"
)
return comp.$el as HTMLElement
}
// Adjust navigation width according to route metadata
watch(
() => route,
(to) => {
if (!to.meta?.resizableNavbar) {
navWidth.value = navRestWidth
}
if (to.meta?.resizableNavbar && window.__lastNavSize) {
navWidth.value = window.__lastNavSize
}
},
{ immediate: true }
)
function resizeHandler(e: MouseEvent) {
e.preventDefault()
const el = drawerEl()
const maxWidth = document.body.offsetWidth / 2
const minWidth = 300
document.body.style.cursor = 'ew-resize'
if (!(e.clientX > maxWidth || e.clientX < minWidth)) {
el.style.width = e.clientX + 'px'
window.__lastNavSize = e.clientX
}
}
function onMouseDown(e: MouseEvent) {
e.preventDefault()
const el = drawerEl()
if (e.offsetX < minSize) {
el.style.transition = 'initial'
document.addEventListener('mousemove', resizeHandler, false)
}
}
function onMouseUp(e: MouseEvent) {
e.preventDefault()
const el = drawerEl()
el.style.transition = ''
document.body.style.cursor = ''
navWidth.value = el.style.width
document.removeEventListener('mousemove', resizeHandler, false)
setTimeout(() => eventHub.$emit('resize-viewer'), 300)
}
// Setup resize events
onMounted(() => {
const el = drawerEl()
const drawerBorder = el.querySelector<HTMLElement>('.nav-resizer')
if (drawerBorder) {
drawerBorder.style.cursor = 'ew-resize'
drawerBorder.addEventListener('mousedown', onMouseDown, false)
}
document.addEventListener('mouseup', onMouseUp, false)
})
// Unsub events
onBeforeUnmount(() => {
const el = drawerEl()
const drawerBorder = el.querySelector<HTMLElement>('.nav-resizer')
if (drawerBorder) {
drawerBorder.removeEventListener('mousedown', onMouseDown)
}
document.removeEventListener('mouseup', onMouseUp)
document.removeEventListener('mousemove', resizeHandler)
})
return {
navWidth
}
}
@@ -0,0 +1,13 @@
import { ComposableInvokedOutOfScopeError } from '@/main/lib/core/errors/composition'
import { OverridedMixpanel } from 'mixpanel-browser'
import { getCurrentInstance } from 'vue'
/**
* Get Mixpanel instance (not reactive)
*/
export function useMixpanel(): OverridedMixpanel {
const vm = getCurrentInstance()
if (!vm) throw new ComposableInvokedOutOfScopeError()
return vm.proxy.$mixpanel
}
@@ -0,0 +1,16 @@
import { useRoute } from '@/main/lib/core/composables/router'
import { computed } from 'vue'
/**
* Get embed viewer query params
*/
export function useEmbedViewerQuery() {
const route = useRoute()
const streamId = computed(() => (route.query.stream as string) || null)
const branchName = computed(() => (route.query.branch as string) || null)
const commitId = computed(() => (route.query.commit as string) || null)
const objectId = computed(() => (route.query.object as string) || null)
const transparent = computed(() => route.query.transparent === 'true')
return { streamId, branchName, commitId, objectId, transparent }
}
@@ -0,0 +1,604 @@
import { GetReactiveVarType, Nullable } from '@/helpers/typeHelpers'
import { setupNewViewerInjection } from '@/main/lib/viewer/core/composables/viewer'
import { makeVar, TypePolicies } from '@apollo/client/cache'
import { DefaultViewerParams, Viewer } from '@speckle/viewer'
import emojis from '@/main/store/emojis'
import { has, isArray } from 'lodash'
import { computed, ComputedRef, inject, InjectionKey, provide, Ref } from 'vue'
const ViewerStreamIdKey: InjectionKey<Ref<string>> = Symbol(
'COMMIT_OBJECT_VIEWER_STREAMID'
)
const ViewerResourceIdKey: InjectionKey<Ref<string>> = Symbol(
'COMMIT_OBJECT_VIEWER_RESOURCEID'
)
const ViewerIsEmbedKey: InjectionKey<Ref<boolean>> = Symbol(
'COMMIT_OBJECT_VIEWER_IS_EMBED'
)
type UnknownObject = Record<string, unknown>
type GlobalViewerData = {
viewer: Viewer
container: HTMLElement
initialized: Promise<void>
}
type FilterKeyAndValues = {
filterKey: string
filterValues: string[]
}
/**
* Global CommitObjectViewer viewer instance & container. It's not held in commitObjectViewerState, because it's a
* complex object that keeps mutating itself (triggering Apollo Client errors)
*/
let globalViewerData: Nullable<GlobalViewerData> = null
/**
* Queryable Apollo Client state
*/
const commitObjectViewerState = makeVar({
viewerBusy: false,
appliedFilter: null as Nullable<UnknownObject>,
isolateKey: null as Nullable<string>,
isolateValues: [] as string[],
hideKey: null as Nullable<string>,
hideValues: [] as string[],
colorLegend: {} as UnknownObject,
isolateCategoryKey: null as Nullable<string>,
isolateCategoryValues: [] as string[],
hideCategoryKey: null as Nullable<string>,
hideCategoryValues: [] as string[],
selectedCommentMetaData: null as Nullable<{
id: number
selectionLocation: Record<string, unknown>
}>,
addingComment: false,
preventCommentCollapse: false,
commentReactions: ['❤️', '✏️', '🔥', '⚠️'],
emojis
})
export type StateType = GetReactiveVarType<typeof commitObjectViewerState>
/**
* Merge (through _.merge) these with the rest of your Apollo Client `typePolicies` to set up
* commit object viewer state management
*/
export const statePolicies: TypePolicies = {
Query: {
fields: {
commitObjectViewerState: {
read() {
return commitObjectViewerState()
}
}
}
}
}
/**
* Get current global Commit Object Viewer instance or create one
*/
function getOrInitViewerData(): GlobalViewerData {
if (globalViewerData) return globalViewerData
const container = document.createElement('div')
container.id = 'renderer'
container.className = 'viewer-container'
container.style.display = 'inline-block'
const viewer = new Viewer(container, DefaultViewerParams)
const initPromise = viewer.init()
globalViewerData = {
viewer,
container,
initialized: initPromise
}
return globalViewerData
}
function getInitializedViewer(): Viewer {
if (!globalViewerData?.viewer) {
throw new Error('Attempting to access viewer before it has been initialized')
}
return globalViewerData.viewer
}
/**
* Composable that returns the global Commit Object Viewer instance and injects it into child components
*/
export function setupCommitObjectViewer(reactiveMainProps: {
streamId: Ref<string>
resourceId: Ref<string>
isEmbed: Ref<boolean>
}) {
const { streamId, resourceId, isEmbed } = reactiveMainProps
// Set up and inject viewer
const viewerData = getOrInitViewerData()
const { viewer, container, isInitialized, isInitializedPromise } =
setupNewViewerInjection({
viewer: viewerData.viewer,
container: viewerData.container,
initPromise: viewerData.initialized
})
// Inject main parameters into child components
provide(ViewerStreamIdKey, streamId)
provide(ViewerResourceIdKey, resourceId)
provide(ViewerIsEmbedKey, isEmbed)
return { viewer, container, isInitialized, isInitializedPromise }
}
/**
* Inject the Commit Object Viewer instance's main parameters
*/
export function useCommitObjectViewerParams() {
const injectedStreamId = inject(ViewerStreamIdKey)
const injectedResourceId = inject(ViewerResourceIdKey)
const injectedIsEmbed = inject(ViewerIsEmbedKey)
const buildSafeRef = <T>(ref: Ref<T> | undefined): ComputedRef<T> =>
computed(() => {
if (!ref) {
throw new Error(
"Couldn't resolve Commit Object Viewer injected state! Is it properly set up??"
)
}
return ref.value
})
const streamId = buildSafeRef(injectedStreamId)
const resourceId = buildSafeRef(injectedResourceId)
const isEmbed = buildSafeRef(injectedIsEmbed)
return { streamId, resourceId, isEmbed }
}
/*
* STATE MODIFICATION FUNCTIONS:
*/
function updateState(newValues: Partial<StateType>) {
commitObjectViewerState({
...commitObjectViewerState(),
...newValues
})
}
export function setIsViewerBusy(isBusy: boolean) {
updateState({ viewerBusy: isBusy })
}
export function setIsAddingComment(isAddingComment: boolean) {
updateState({ addingComment: isAddingComment })
}
/**
* Note: We should not set the entire comment here, because we mutate comments in multiple places
* and that would cause a cache mutation
*/
export function setSelectedCommentMetaData(
comment: { id: number; data: { location: Record<string, unknown> } } | null
) {
updateState({
selectedCommentMetaData: comment
? { id: comment.id, selectionLocation: comment.data.location }
: null
})
}
export function isolateObjects(params: FilterKeyAndValues) {
const { filterKey, filterValues } = params
const state = { ...commitObjectViewerState() }
state.hideKey = null
state.hideValues = []
if (state.isolateKey !== filterKey) {
state.isolateValues = []
}
state.isolateKey = filterKey
state.isolateValues = [...new Set([...state.isolateValues, ...filterValues])]
if (state.isolateValues.length === 0) {
state.appliedFilter = null
} else {
state.appliedFilter = {
filterBy: { [filterKey]: { includes: state.isolateValues } },
ghostOthers: true
}
}
updateState(state)
getInitializedViewer().applyFilter(state.appliedFilter)
}
export function unisolateObjects(params: FilterKeyAndValues) {
const { filterKey, filterValues } = params
const state = { ...commitObjectViewerState() }
state.hideKey = null
state.hideValues = []
if (state.isolateKey !== filterKey) {
state.isolateValues = []
}
state.isolateKey = filterKey
state.isolateValues = state.isolateValues.filter(
(val) => filterValues.indexOf(val) === -1
)
if (state.isolateValues.length === 0) {
state.appliedFilter = null
} else {
state.appliedFilter = {
filterBy: { [filterKey]: { includes: state.isolateValues } },
ghostOthers: true
}
}
updateState(state)
getInitializedViewer().applyFilter(state.appliedFilter)
}
export function hideObjects(params: FilterKeyAndValues) {
const { filterKey, filterValues } = params
const state = { ...commitObjectViewerState() }
state.isolateKey = null
state.isolateValues = []
if (state.hideKey !== filterKey) {
state.hideValues = []
}
state.hideKey = filterKey
state.hideValues = [...new Set([...filterValues, ...state.hideValues])]
if (state.hideValues.length === 0) {
state.appliedFilter = null
} else {
state.appliedFilter = {
filterBy: { [filterKey]: { excludes: state.hideValues } }
}
}
updateState(state)
getInitializedViewer().applyFilter(state.appliedFilter)
}
export function showObjects(params: FilterKeyAndValues) {
const { filterKey, filterValues } = params
const state = { ...commitObjectViewerState() }
state.isolateKey = null
state.isolateValues = []
if (state.hideKey !== filterKey) {
state.hideValues = []
}
state.hideKey = filterKey
state.hideValues = state.hideValues.filter((val) => filterValues.indexOf(val) === -1)
if (state.hideValues.length === 0) {
state.appliedFilter = null
} else {
state.appliedFilter = {
filterBy: { [filterKey]: { excludes: state.hideValues } }
}
}
updateState(state)
getInitializedViewer().applyFilter(state.appliedFilter)
}
function resetInternalHideIsolateObjectState() {
updateState({
isolateKey: null,
isolateValues: [],
hideKey: null,
hideValues: []
})
}
export async function isolateCategoryToggle(params: {
filterKey: string
filterValue: string
allValues: string[]
colorBy?: Record<string, unknown> | false
}) {
resetInternalHideIsolateObjectState()
const { filterKey, filterValue, allValues, colorBy = false } = params
const state = { ...commitObjectViewerState() }
const viewer = getInitializedViewer()
state.hideCategoryKey = null
state.hideCategoryValues = []
if (filterKey !== state.isolateCategoryKey) {
state.isolateCategoryValues = []
}
state.isolateCategoryKey = filterKey
const isolateCategoryValues = [...state.isolateCategoryValues]
const indx = isolateCategoryValues.indexOf(filterValue)
if (indx === -1) {
isolateCategoryValues.push(filterValue)
} else {
isolateCategoryValues.splice(indx, 1)
}
state.isolateCategoryValues = isolateCategoryValues
if (
(state.isolateCategoryValues.length === 0 ||
state.isolateCategoryValues.length === allValues.length) &&
!colorBy
) {
state.appliedFilter = null
updateState(state)
viewer.applyFilter(state.appliedFilter)
return
}
if (state.isolateCategoryValues.length === 0 && colorBy) {
state.appliedFilter = {
colorBy: { type: 'category', property: filterKey }
}
}
if (state.isolateCategoryValues.length !== 0) {
state.appliedFilter = {
ghostOthers: true,
filterBy: { [filterKey]: state.isolateCategoryValues },
colorBy: colorBy ? { type: 'category', property: filterKey } : null
}
}
if (
state.isolateCategoryValues.length === allValues.length &&
state.appliedFilter?.filterBy
) {
const newAppliedFilter = { ...state.appliedFilter }
delete newAppliedFilter.filterBy
state.appliedFilter = newAppliedFilter
}
const res = await viewer.applyFilter(state.appliedFilter)
state.colorLegend = res.colorLegend
updateState(state)
}
export async function hideCategoryToggle(params: {
filterKey: string
filterValue: string
colorBy?: Record<string, unknown> | false
}) {
resetInternalHideIsolateObjectState()
const { filterKey, filterValue, colorBy = false } = params
const state = { ...commitObjectViewerState() }
const viewer = getInitializedViewer()
state.isolateCategoryKey = null
state.isolateCategoryValues = []
if (filterKey !== state.hideCategoryKey) {
state.hideCategoryValues = []
}
state.hideCategoryKey = filterKey
const hideCategoryValues = [...state.hideCategoryValues]
const indx = hideCategoryValues.indexOf(filterValue)
if (indx === -1) {
hideCategoryValues.push(filterValue)
} else {
hideCategoryValues.splice(indx, 1)
}
state.hideCategoryValues = hideCategoryValues
if (state.hideCategoryValues.length === 0 && !colorBy) {
state.appliedFilter = null
updateState(state)
viewer.applyFilter(state.appliedFilter)
return
}
if (state.hideCategoryValues.length === 0 && colorBy) {
state.appliedFilter = {
colorBy: { type: 'category', property: filterKey }
}
}
if (state.hideCategoryValues.length !== 0) {
state.appliedFilter = {
filterBy: { [filterKey]: { not: state.hideCategoryValues } },
colorBy: colorBy ? { type: 'category', property: filterKey } : null
}
}
const res = await viewer.applyFilter(state.appliedFilter)
state.colorLegend = res.colorLegend
updateState(state)
}
export async function toggleColorByCategory(params: { filterKey: string }) {
const { filterKey } = params
const state = { ...commitObjectViewerState() }
const viewer = getInitializedViewer()
if (state.appliedFilter && state.appliedFilter.colorBy) {
state.appliedFilter = {
...state.appliedFilter,
colorBy: null
}
} else {
state.appliedFilter = {
...state.appliedFilter,
colorBy: { type: 'category', property: filterKey }
}
}
const res = await viewer.applyFilter(state.appliedFilter)
state.colorLegend = res.colorLegend
updateState(state)
}
function resetInternalCategoryObjectState() {
updateState({
isolateCategoryKey: null,
isolateCategoryValues: [],
hideCategoryKey: null,
hideCategoryValues: []
})
}
export function setNumericFilter(params: {
filterKey: string
minValue: number
maxValue: number
gradientColors?: string[]
}) {
resetInternalHideIsolateObjectState()
resetInternalCategoryObjectState()
const {
filterKey,
minValue,
maxValue,
gradientColors = ['#3F5EFB', '#FC466B']
} = params
const state = { ...commitObjectViewerState() }
const viewer = getInitializedViewer()
state.appliedFilter = {
ghostOthers: true,
colorBy: {
type: 'gradient',
property: filterKey,
minValue,
maxValue,
gradientColors
},
filterBy: { [filterKey]: { gte: minValue, lte: maxValue } }
}
updateState(state)
viewer.applyFilter(state.appliedFilter)
}
// not sure if these filter types are 100% correct, I reverse engineered them
// from the code
type FilterByValue =
| {
gte: number
lte: number
}
| string[]
| { not: string[] }
export type Filter = {
filterBy?: {
__parents?: {
includes?: string[]
excludes?: string[]
}
} & {
[by: string]: FilterByValue
}
colorBy?: {
type: string
property: string
}
ghostOthers?: boolean
}
export async function setFilterDirectly(params: { filter: Filter }) {
const { filter } = params
const isNotFilter = (filterByVal: FilterByValue): filterByVal is { not: string[] } =>
has(filterByVal, 'not')
const filterBy = filter.filterBy
if (filterBy && filterBy.__parents) {
if (filterBy.__parents.includes) {
isolateObjects({
filterKey: '__parents',
filterValues: filterBy.__parents.includes
})
return
}
if (filterBy.__parents.excludes) {
hideObjects({
filterKey: '__parents',
filterValues: filterBy.__parents.excludes
})
return
}
} else if (filter.ghostOthers && filterBy) {
// means it's isolate by category or numeric filter
const filterByKey = Object.keys(filterBy || {})[0]
const filterVal = filterBy[filterByKey]
if (
filter.colorBy &&
filter.colorBy.type === 'gradient' &&
!isArray(filterVal) &&
!isNotFilter(filterVal)
) {
setNumericFilter({
filterKey: filterByKey,
minValue: filterVal.gte,
maxValue: filterVal.lte
})
} else if (isArray(filterVal)) {
for (const val of filterVal) {
const f = {
filterKey: filterByKey,
filterValue: val,
allValues: [],
colorBy: filter.colorBy
}
isolateCategoryToggle(f)
}
}
} else if (filterBy) {
const filterByKey = Object.keys(filterBy || {})[0]
const filterVal = filterBy[filterByKey]
if (isNotFilter(filterVal)) {
const values = filterVal.not
for (const val of values) {
const f = {
filterKey: filterByKey,
filterValue: val,
allValues: [],
colorBy: filter.colorBy
}
hideCategoryToggle(f)
}
}
} else if (filter.colorBy) {
toggleColorByCategory({ filterKey: filter.colorBy.property })
}
}
export function resetFilter() {
const viewer = getInitializedViewer()
resetInternalHideIsolateObjectState()
resetInternalCategoryObjectState()
updateState({
appliedFilter: null,
preventCommentCollapse: true
})
viewer.applyFilter(null)
}
export function setPreventCommentCollapse(shouldPrevent: boolean) {
updateState({
preventCommentCollapse: shouldPrevent
})
}
@@ -0,0 +1,51 @@
import { Viewer } from '@speckle/viewer'
import { inject, InjectionKey, Ref, ref, provide } from 'vue'
// Keys you can use in the Viewer family of components to inject the viewer, its container and its init state
export const ViewerKey: InjectionKey<Ref<Viewer>> = Symbol('VIEWER_INSTANCE')
export const ViewerInitializedKey: InjectionKey<Ref<boolean>> = Symbol(
'VIEWER_INSTANCE_INITIALIZED'
)
export const ViewerInitializedPromiseKey: InjectionKey<Promise<boolean>> = Symbol(
'VIEWER_INSTANCE_INITIALIZED_PROMISE'
)
export const ViewerContainerKey: InjectionKey<Ref<HTMLElement>> =
Symbol('VIEWER_CONTAINER')
/**
* Inject viewer instance (it should be provided in an ancestor component using setupViewerInjection())
*/
export function useInjectedViewer() {
// force casting, cause we know for a fact that these injections won't be undefined - handling the "or undefined" check everywhere
// is going to be a pain in the ass
const viewer = inject(ViewerKey) as Ref<Viewer>
const container = inject(ViewerContainerKey) as Ref<HTMLElement>
const isInitialized = inject(ViewerInitializedKey) as Ref<boolean>
const isInitializedPromise = inject(ViewerInitializedPromiseKey) as Promise<boolean>
return { viewer, container, isInitialized, isInitializedPromise }
}
/**
* Pass in a newly created Viewer instance and its container for injection down into child components
* (through useInjectedViewer() or the injection keys manually).
*/
export function setupNewViewerInjection(params: {
viewer: Viewer
container: HTMLElement
initPromise: Promise<void>
}) {
const viewer: Ref<Viewer> = ref(params.viewer) as Ref<Viewer>
const container = ref<HTMLElement>(params.container)
const isInitialized = ref(false)
const isInitializedPromise = params.initPromise.then(
() => (isInitialized.value = true)
)
provide(ViewerKey, viewer)
provide(ViewerContainerKey, container)
provide(ViewerInitializedKey, isInitialized)
provide(ViewerInitializedPromiseKey, isInitializedPromise)
return { viewer, container, isInitialized, isInitializedPromise }
}
@@ -0,0 +1,24 @@
import { Vector3 } from 'three'
import { Viewer } from '@speckle/viewer'
import CameraControls from 'camera-controls'
// we're relying on a private property here, which we shouldn't do
// (I'm just migrating the function over from a previous file, I didn't write it)
type RealCameraControls = CameraControls & { _zoom: number }
export function getCamArray(viewer: Viewer) {
const controls = viewer.cameraHandler.activeCam.controls as RealCameraControls
const pos = controls.getPosition(new Vector3())
const target = controls.getTarget(new Vector3())
const c = [
parseFloat(pos.x.toFixed(5)),
parseFloat(pos.y.toFixed(5)),
parseFloat(pos.z.toFixed(5)),
parseFloat(target.x.toFixed(5)),
parseFloat(target.y.toFixed(5)),
parseFloat(target.z.toFixed(5)),
viewer.cameraHandler.activeCam.name === 'ortho' ? 1 : 0,
controls._zoom
]
return c
}
@@ -80,6 +80,7 @@
import { gql } from '@apollo/client/core'
import UserAvatar from '@/main/components/auth/UserAvatarAuthoriseApp'
import UserAvatarIcon from '@/main/components/common/UserAvatarIcon'
import { AppLocalStorage } from '@/utils/localStorage'
export default {
name: 'AuthorizeApp',
@@ -124,7 +125,7 @@ export default {
return `${this.app.redirectUrl}?denied=true`
},
selfUserId() {
return localStorage.getItem('uuid')
return AppLocalStorage.get('uuid')
}
},
watch: {
@@ -146,7 +147,7 @@ export default {
window.location.replace(
`${window.location.origin}/auth/accesscode?appId=${this.app.id}&challenge=${
this.$route.params.challenge
}&token=${localStorage.getItem('AuthToken')}&suuid=${this.$route.query.suuid}`
}&token=${AppLocalStorage.get('AuthToken')}&suuid=${this.$route.query.suuid}`
)
}
}
@@ -110,6 +110,7 @@ import {
getInviteTokenFromRoute,
processSuccessfulAuth
} from '@/main/lib/auth/services/authService'
import { AppLocalStorage } from '@/utils/localStorage'
export default {
name: 'TheLogin',
@@ -193,7 +194,7 @@ export default {
if (!challenge && this.appId === 'spklwebapp') {
this.challenge = randomString(10)
localStorage.setItem('appChallenge', this.challenge)
AppLocalStorage.set('appChallenge', this.challenge)
} else if (challenge) {
this.challenge = challenge
}
@@ -191,6 +191,7 @@ import {
getInviteTokenFromRoute,
processSuccessfulAuth
} from '@/main/lib/auth/services/authService'
import { AppLocalStorage } from '@/utils/localStorage'
export default {
name: 'TheRegistration',
@@ -289,7 +290,7 @@ export default {
if (!challenge && this.appId === 'spklwebapp') {
this.challenge = randomString({ length: 10 })
localStorage.setItem('appChallenge', this.challenge)
AppLocalStorage.set('appChallenge', this.challenge)
} else if (challenge) {
this.challenge = challenge
}
@@ -1,5 +1,5 @@
<template>
<div>
<div class="commit-object-viewer">
<div v-if="(isMultiple || isCommit || isObject) && !singleResourceError">
<commit-toolbar
v-if="isCommit"
@@ -9,77 +9,86 @@
<object-toolbar v-if="isObject" :stream="resources[0].data" />
<multiple-resources-toolbar
v-if="isMultiple"
:stream="{ name: resources[0].data.name, id: $route.params.streamId }"
:stream="{ name: resources[0].data.name, id: streamId }"
:resources="resources"
/>
<portal v-if="canRenderNavPortal" to="nav">
<div v-if="!$loggedIn()" class="px-4 my-2">
<v-btn small block color="primary" @click="$loginAndSetRedirect()">
Sign In
</v-btn>
</div>
<v-list nav dense class="mt-0 pt-0">
<v-list-item
v-if="isCommit"
link
:to="`/streams/${$route.params.streamId}/branches/${resources[0].data.commit.branchName}`"
class=""
>
<v-list-item-icon>
<v-icon small class>mdi-arrow-left-drop-circle</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="font-weight-bold">
<v-icon small class="mr-1 caption">mdi-source-branch</v-icon>
{{ resources[0].data.commit.branchName }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
v-if="isObject || isMultiple"
link
exact
:to="`/streams/${$route.params.streamId}`"
class=""
>
<v-list-item-icon>
<v-icon small class>mdi-arrow-left-drop-circle</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="font-weight-bold">
<v-icon small class="mr-1 caption">mdi-home</v-icon>
Stream Home
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
<prioritized-portal to="nav" identity="stream-commit-viewer" :priority="2">
<commit-object-viewer-scope
:stream-id="streamId"
:resource-id="resourceId"
:is-embed="isEmbed"
>
<template v-if="!isEmbed">
<div v-if="!$loggedIn()" class="px-4 my-2">
<v-btn small block color="primary" @click="$loginAndSetRedirect()">
Sign In
</v-btn>
</div>
<v-list nav dense class="mt-0 pt-0">
<v-list-item
v-if="isCommit"
link
:to="`/streams/${streamId}/branches/${resources[0].data.commit.branchName}`"
class=""
>
<v-list-item-icon>
<v-icon small class>mdi-arrow-left-drop-circle</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="font-weight-bold">
<v-icon small class="mr-1 caption">mdi-source-branch</v-icon>
{{ resources[0].data.commit.branchName }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
v-if="isObject || isMultiple"
link
exact
:to="`/streams/${streamId}`"
class=""
>
<v-list-item-icon>
<v-icon small class>mdi-arrow-left-drop-circle</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="font-weight-bold">
<v-icon small class="mr-1 caption">mdi-home</v-icon>
Stream Home
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</template>
<!-- Loaded resources -->
<resource-group
:resources="resources"
@remove="removeResource"
@add-resource="addResource"
@show-add-overlay="showAddOverlay = true"
/>
<!-- Loaded resources -->
<resource-group
:resources="resources"
:allow-add="!isEmbed"
@remove="removeResource"
@add-resource="addResource"
@show-add-overlay="showAddOverlay = true"
/>
<!-- <v-divider v-if="isMultiple" class="my-4" /> -->
<portal-target name="comments"></portal-target>
<!-- Views display -->
<views-display v-if="views.length !== 0" :views="views" class="mt-4" />
<!-- <v-divider v-if="isMultiple" class="my-4" /> -->
<portal-target name="comments"></portal-target>
<!-- Views display -->
<views-display v-if="views.length !== 0" :views="views" class="mt-4" />
<!-- Filters display -->
<viewer-filters
class="mt-4"
:props="objectProperties"
:source-application="
resources
.filter((r) => r.type === 'commit')
.map((r) => r.data.commit.sourceApplication)
.join(',')
"
/>
</portal>
<!-- Filters display -->
<viewer-filters
class="mt-4"
:props="objectProperties"
:source-application="
resources
.filter((r) => r.type === 'commit')
.map((r) => r.data.commit.sourceApplication)
.join(',')
"
/>
</commit-object-viewer-scope>
</prioritized-portal>
<!-- Preview image -->
<v-fade-transition>
@@ -88,14 +97,14 @@
:style="`
height: 100vh;
width: 100%;
${!$vuetify.breakpoint.smAndDown ? 'top: -64px;' : 'top: -56px;'}
${topOffsetStyle}
left: 0px;
position: absolute;
opacity: 0.7;
filter: blur(4px);
`"
:height="420"
:url="`/preview/${$route.params.streamId}/objects/${
:url="`/preview/${streamId}/objects/${
isCommit
? resources[0].data.commit.referencedObject
: resources[0].data.object.id
@@ -104,9 +113,7 @@
</v-fade-transition>
<div
:style="`height: 100vh; width: 100%; ${
!$vuetify.breakpoint.smAndDown ? 'top: -64px;' : 'top: -56px;'
} left: 0px; position: absolute`"
:style="`height: 100vh; width: 100%; ${topOffsetStyle} left: 0px; position: absolute`"
>
<speckle-viewer @load-progress="captureProgress" @selection="captureSelect" />
</div>
@@ -115,7 +122,7 @@
:style="`
height: 100vh;
width: 100%;
${!$vuetify.breakpoint.smAndDown ? 'top: -64px;' : 'top: -56px;'}
${topOffsetStyle}
left: 22px;
position: absolute;
z-index: 10;
@@ -125,7 +132,7 @@
v-show="selectionData.length !== 0"
:key="'one'"
:objects="selectionData"
:stream-id="$route.params.streamId"
:stream-id="streamId"
@clear-selection="selectionData = []"
/>
</div>
@@ -143,7 +150,7 @@
:style="`
height: 100vh;
width: 100%;
${!$vuetify.breakpoint.smAndDown ? 'top: -64px;' : 'top: -56px;'}
${topOffsetStyle}
left: 0;
position: absolute;
z-index: 4;
@@ -154,7 +161,7 @@
>
<viewer-bubbles key="a" />
<comments-overlay key="c" @add-resources="addResources" />
<comment-add-overlay key="b" />
<comment-add-overlay v-if="!isEmbed" key="b" />
</div>
<!--
@@ -198,7 +205,7 @@
<div v-else-if="singleResourceError">
<error-placeholder error-type="404">
<h2>
<code>{{ $route.params.resourceId }}</code>
<code>{{ resourceId }}</code>
not found.
</h2>
</error-placeholder>
@@ -210,7 +217,7 @@
style="z-index: 10000"
>
<stream-overlay-viewer
:stream-id="$route.params.streamId"
:stream-id="streamId"
@add-resource="addResource"
@close="showAddOverlay = false"
/>
@@ -225,93 +232,172 @@
</v-dialog>
</div>
</template>
<script>
<script lang="ts">
import { computed, defineComponent, toRefs } from 'vue'
import debounce from 'lodash/debounce'
import streamCommitQuery from '@/graphql/commit.gql'
import streamObjectQuery from '@/graphql/objectSingleNoData.gql'
import SpeckleViewer from '@/main/components/common/SpeckleViewer.vue' // do not import async
import { resourceType } from '@/plugins/resourceIdentifier'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
} from '@/main/utils/portalStateManager'
Filter,
setFilterDirectly,
setIsViewerBusy,
setupCommitObjectViewer
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'
import {
StreamCommitQueryQuery,
StreamObjectNoDataQuery
} from '@/graphql/generated/graphql'
import { Get } from 'type-fest'
import { has } from 'lodash'
import { Nullable } from '@/helpers/typeHelpers'
import { getCamArray } from '@/main/lib/viewer/core/helpers/cameraHelper'
import CommitObjectViewerScope from '@/main/components/viewer/CommitObjectViewerScope.vue'
import PrioritizedPortal from '@/main/components/common/utility/PrioritizedPortal.vue'
export default {
type ErroredResourceData = {
error: boolean
message: string
}
type CommitResourceData = NonNullable<Get<StreamCommitQueryQuery, 'stream'>>
type ObjectResourceData = NonNullable<Get<StreamObjectNoDataQuery, 'stream'>>
type AllSupportedDataTypes =
| ErroredResourceData
| CommitResourceData
| ObjectResourceData
type ResourceTypeValue = 'commit' | 'object'
type ResourceObjectType<T> = {
type: ResourceTypeValue
id: string
data: T
}
const isErrorResource = (
resource: ResourceObjectType<unknown>
): resource is ResourceObjectType<ErroredResourceData> => has(resource.data, 'error')
const isCommitResource = (
resource: ResourceObjectType<unknown>
): resource is ResourceObjectType<CommitResourceData> => resource.type === 'commit'
const isObjectResource = (
resource: ResourceObjectType<unknown>
): resource is ResourceObjectType<ObjectResourceData> => resource.type === 'object'
export default defineComponent({
name: 'CommitObjectViewer',
components: {
SpeckleViewer,
CommitToolbar: () => import('@/main/toolbars/CommitToolbar'),
ObjectToolbar: () => import('@/main/toolbars/ObjectToolbar'),
MultipleResourcesToolbar: () => import('@/main/toolbars/MultipleResourcesToolbar'),
CommitEdit: () => import('@/main/dialogs/CommitEdit'),
CommitObjectViewerScope,
PrioritizedPortal,
CommitToolbar: () => import('@/main/toolbars/CommitToolbar.vue'),
ObjectToolbar: () => import('@/main/toolbars/ObjectToolbar.vue'),
MultipleResourcesToolbar: () =>
import('@/main/toolbars/MultipleResourcesToolbar.vue'),
CommitEdit: () => import('@/main/dialogs/CommitEdit.vue'),
StreamOverlayViewer: () =>
import('@/main/components/viewer/dialogs/StreamOverlayViewer.vue'),
ErrorPlaceholder: () => import('@/main/components/common/ErrorPlaceholder'),
PreviewImage: () => import('@/main/components/common/PreviewImage'),
ViewerControls: () => import('@/main/components/viewer/ViewerControls'),
ObjectSelection: () => import('@/main/components/viewer/ObjectSelection'),
ResourceGroup: () => import('@/main/components/viewer/ResourceGroup'),
ViewsDisplay: () => import('@/main/components/viewer/ViewsDisplay'),
ErrorPlaceholder: () => import('@/main/components/common/ErrorPlaceholder.vue'),
PreviewImage: () => import('@/main/components/common/PreviewImage.vue'),
ViewerControls: () => import('@/main/components/viewer/ViewerControls.vue'),
ObjectSelection: () => import('@/main/components/viewer/ObjectSelection.vue'),
ResourceGroup: () => import('@/main/components/viewer/ResourceGroup.vue'),
ViewsDisplay: () => import('@/main/components/viewer/ViewsDisplay.vue'),
ViewerFilters: () => import('@/main/components/viewer/ViewerFilters.vue'),
ViewerBubbles: () => import('@/main/components/viewer/ViewerBubbles.vue'),
CommentAddOverlay: () => import('@/main/components/viewer/CommentAddOverlay'),
CommentsOverlay: () => import('@/main/components/viewer/CommentsOverlay')
CommentAddOverlay: () => import('@/main/components/viewer/CommentAddOverlay.vue'),
CommentsOverlay: () => import('@/main/components/viewer/CommentsOverlay.vue')
},
props: {
streamId: {
type: String,
required: true
},
/**
* Commit or Object ID
*/
resourceId: {
type: String,
required: true
},
isEmbed: {
type: Boolean,
default: false
}
},
setup(props) {
const { viewer } = setupCommitObjectViewer(toRefs(props))
const { result: viewerStateResult } = useQuery(gql`
query {
commitObjectViewerState @client {
appliedFilter
}
}
`)
const viewerState = computed(
() => viewerStateResult.value?.commitObjectViewerState || {}
)
return {
viewer,
viewerState
}
},
mixins: [
buildPortalStateMixin([STANDARD_PORTAL_KEYS.Nav], 'stream-commit-viewer', 1)
],
data: () => ({
firstCallToCam: false,
camToSet: null as Nullable<number[]>,
filterToSet: null as Nullable<Filter>,
loadedModel: false,
loadProgress: 0,
showCommitEditDialog: false,
selectionData: [],
views: [],
objectProperties: null,
hiddenObjects: [],
isolatedObjects: [],
showVisReset: false,
resourceType: null,
resources: [],
selectionData: [] as Record<string, unknown>[],
views: [] as Record<string, unknown>[],
objectProperties: null as Nullable<Record<string, unknown>>,
resources: [] as ResourceObjectType<AllSupportedDataTypes>[],
showAddOverlay: false,
viewerBusy: false
}),
computed: {
isCommit() {
topOffsetStyle(): string {
if (this.isEmbed) return 'top: 0;'
return !this.$vuetify.breakpoint.smAndDown ? 'top: -64px;' : 'top: -56px;'
},
isCommit(): boolean {
if (this.resources.length === 0) return false
if (this.resources.length === 1 && this.resources[0].type === 'commit')
return true
return false
},
isObject() {
isObject(): boolean {
if (this.resources.length === 0) return false
if (this.resources.length === 1 && this.resources[0].type === 'object')
return true
return false
},
isMultiple() {
isMultiple(): boolean {
if (this.resources.length === 0) return false
if (this.resources.length > 1) return true
return false
},
singleResourceError() {
return this.resources.length === 1 && this.resources[0].data.error
singleResourceError(): boolean {
if (this.resources.length !== 1) return false
const resource = this.resources[0]
if (!isErrorResource(resource)) return false
return resource.data.error
},
overlay(): Nullable<string> {
return this.$route.query.overlay ? (this.$route.query.overlay as string) : null
}
},
watch: {
stream(val) {
if (!val) return
if (
val &&
val.commit &&
val.commit.branchName &&
val.commit.branchName === 'globals'
) {
this.$router.push(
`/streams/${this.$route.params.streamId}/globals/${val.commit.id}`
)
return
}
},
'$store.state.appliedFilter'(val) {
'viewerState.appliedFilter'(val) {
if (!val) {
const fullQuery = { ...this.$route.query }
delete fullQuery.filter
@@ -334,20 +420,20 @@ export default {
async mounted() {
this.$eventHub.$emit('page-load', true)
this.resources.push({
type: resourceType(this.$route.params.resourceId),
id: this.$route.params.resourceId,
type: this.resolveResourceType(this.resourceId),
id: this.resourceId,
data:
resourceType(this.$route.params.resourceId) === 'commit'
? await this.loadCommit(this.$route.params.resourceId)
: await this.loadObject(this.$route.params.resourceId)
this.resolveResourceType(this.resourceId) === 'commit'
? await this.loadCommit(this.resourceId)
: await this.loadObject(this.resourceId)
})
if (this.$route.query.overlay) {
const ids = this.$route.query.overlay.split(',')
if (this.overlay) {
const ids = this.overlay.split(',')
for (const id of ids) {
const cleanedId = id.replace(/\s+/g, '')
if (!cleanedId || cleanedId === '') continue
const resType = resourceType(cleanedId)
const resType = this.resolveResourceType(cleanedId)
this.resources.push({
type: resType,
id: cleanedId,
@@ -359,13 +445,15 @@ export default {
}
}
// If global variables commit, redirect to globals editor page
if (
!this.isEmbed &&
this.resources.length === 1 &&
this.resources[0].type === 'commit' &&
this.resources[0].data.commit.branchName === 'globals'
isCommitResource(this.resources[0]) &&
this.resources[0].data.commit?.branchName === 'globals'
) {
this.$router.push(
`/streams/${this.$route.params.streamId}/globals/${this.resources[0].data.commit.id}`
`/streams/${this.streamId}/globals/${this.resources[0].data.commit.id}`
)
return
}
@@ -375,40 +463,46 @@ export default {
this.camToSet = null
this.filterToSet = null
if (this.$route.query && this.$route.query.c) {
this.camToSet = JSON.parse(this.$route.query.c)
if (this.$route.query?.c) {
this.camToSet = JSON.parse(this.$route.query.c as string)
}
if (this.$route.query && this.$route.query.filter) {
this.filterToSet = JSON.parse(this.$route.query.filter)
if (this.$route.query?.filter) {
this.filterToSet = JSON.parse(this.$route.query.filter as string)
}
setTimeout(() => {
for (const resource of this.resources) {
if (resource.data.error) continue
this.loadModel(
resource.type === 'commit'
? resource.data.commit.referencedObject
: resource.data.object.id
)
if (isErrorResource(resource)) continue
let modelId: string | undefined = undefined
if (isCommitResource(resource)) {
modelId = resource.data.commit?.referencedObject
} else if (isObjectResource(resource)) {
modelId = resource.data.object?.id
}
if (modelId) {
this.loadModel(modelId)
}
}
window.__viewer.on('busy', (val) => {
this.$store.commit('setViewerBusy', { viewerBusyState: val })
this.viewer.on('busy', (val: boolean) => {
setIsViewerBusy(!!val)
this.viewerBusy = val
if (!val && this.camToSet) {
setTimeout(() => {
if (!this.camToSet) return
if (this.camToSet[6] === 1) {
window.__viewer.toggleCameraProjection()
this.viewer.toggleCameraProjection()
}
window.__viewer.interactions.setLookAt(
this.viewer.interactions.setLookAt(
{ x: this.camToSet[0], y: this.camToSet[1], z: this.camToSet[2] }, // position
{ x: this.camToSet[3], y: this.camToSet[4], z: this.camToSet[5] } // target
)
if (this.camToSet[6] === 1) {
window.__viewer.cameraHandler.activeCam.controls.zoom(
this.camToSet[7],
true
)
this.viewer.cameraHandler.activeCam.controls.zoom(this.camToSet[7], true)
}
this.camToSet = null
}, 200)
@@ -416,13 +510,15 @@ export default {
if (!val && this.filterToSet) {
setTimeout(() => {
this.$store.commit('setFilterDirect', { filter: this.filterToSet })
if (!this.filterToSet) return
setFilterDirectly({ filter: this.filterToSet })
this.filterToSet = null
}, 200)
}
})
window.__viewer.cameraHandler.controls.addEventListener(
this.viewer.cameraHandler.controls.addEventListener(
'rest',
debounce(() => {
if (!(this.$route.name === 'commit' || this.$route.name === 'object')) {
@@ -434,19 +530,7 @@ export default {
}
if (this.camToSet) return
const controls = window.__viewer.cameraHandler.activeCam.controls
const pos = controls.getPosition()
const target = controls.getTarget()
const c = [
parseFloat(pos.x.toFixed(5)),
parseFloat(pos.y.toFixed(5)),
parseFloat(pos.z.toFixed(5)),
parseFloat(target.x.toFixed(5)),
parseFloat(target.y.toFixed(5)),
parseFloat(target.z.toFixed(5)),
window.__viewer.cameraHandler.activeCam.name === 'ortho' ? 1 : 0,
controls._zoom
]
const c = getCamArray(this.viewer)
const fullQuery = { ...this.$route.query }
delete fullQuery.c
this.$router
@@ -457,14 +541,19 @@ export default {
.catch(() => {})
}, 1000)
)
this.$emit('models-loaded')
}, 300)
},
methods: {
async loadCommit(id) {
resolveResourceType(resourceId: string): ResourceTypeValue {
return resourceId.length === 10 ? 'commit' : 'object'
},
async loadCommit(id: string) {
try {
const res = await this.$apollo.query({
query: streamCommitQuery,
variables: { streamId: this.$route.params.streamId, id }
variables: { streamId: this.streamId, id }
})
if (res.data.stream.commit === null) throw new Error()
return res.data.stream
@@ -473,11 +562,11 @@ export default {
return { error: true, message: `Failed to load commit ${id}` }
}
},
async loadObject(id) {
async loadObject(id: string) {
try {
const res = await this.$apollo.query({
query: streamObjectQuery,
variables: { streamId: this.$route.params.streamId, id }
variables: { streamId: this.streamId, id }
})
if (res.data.stream.object === null) throw new Error()
return res.data.stream
@@ -486,28 +575,24 @@ export default {
return { error: true, message: `Failed to load object ${id}` }
}
},
async loadModel(objectId) {
if (!window.__viewer) {
this.$eventHub.$emit('notification', {
text: 'Error in rendering page (no __viewer found). Please refresh.'
})
}
await window.__viewer.loadObject(
`${window.location.origin}/streams/${this.$route.params.streamId}/objects/${objectId}`
async loadModel(objectId: string) {
await this.viewer.loadObject(
`${window.location.origin}/streams/${this.streamId}/objects/${objectId}`
)
window.__viewer.zoomExtents(undefined, true)
this.viewer.zoomExtents(undefined, true)
this.loadedModel = true
this.setFilters()
this.setViews()
},
async addResources(ids) {
async addResources(ids: string[]) {
for (const id of ids) {
await this.addResource(id)
}
},
async addResource(resId) {
async addResource(resId: string) {
this.showAddOverlay = false
const resType = resourceType(resId)
const resType = this.resolveResourceType(resId)
const existing = this.resources.findIndex((res) => res.id === resId)
if (existing !== -1) {
@@ -535,8 +620,8 @@ export default {
// TODO add to url
const fullQuery = { ...this.$route.query }
delete fullQuery.overlay
if (this.$route.query.overlay) {
const arr = this.$route.query.overlay
if (this.overlay) {
const arr = this.overlay
.split(',')
.map((id) => id.replace(/\s+/g, ''))
.filter((id) => id && id !== '' && id !== resource.id)
@@ -558,16 +643,18 @@ export default {
: resource.data.object.id
)
},
async removeResource(resource) {
async removeResource(resource: ResourceObjectType<AllSupportedDataTypes>) {
const index = this.resources.findIndex((res) => resource.id === res.id)
if (index === -1) return // err
if (!resource.data.error) {
if (
!isErrorResource(resource) &&
(isCommitResource(resource) || isObjectResource(resource))
) {
const url = `${window.location.origin}/streams/${resource.data.id}/objects/${
resource.type === 'commit'
? resource.data.commit.referencedObject
: resource.data.object.id
isCommitResource(resource)
? resource.data.commit?.referencedObject
: resource.data.object?.id
}`
this.$mixpanel.track('Viewer Action', {
@@ -576,14 +663,14 @@ export default {
resourceType: resource.type
})
await window.__viewer.unloadObject(url)
window.__viewer.zoomExtents(undefined, true)
await this.viewer.unloadObject(url)
this.viewer.zoomExtents(undefined, true)
}
this.resources.splice(index, 1)
this.setFilters()
this.setViews()
if (this.$route.query.overlay) {
const arr = this.$route.query.overlay
if (this.overlay) {
const arr = this.overlay
.split(',')
.map((id) => id.replace(/\s+/g, ''))
.filter((id) => id && id !== '' && id !== resource.id)
@@ -604,25 +691,25 @@ export default {
},
setViews() {
this.views.splice(0, this.views.length)
this.views.push(...window.__viewer.sceneManager.views)
this.views.push(...this.viewer.sceneManager.views)
},
async setFilters() {
try {
// repopulate object props
this.objectProperties = await window.__viewer.getObjectsProperties()
this.objectProperties = await this.viewer.getObjectsProperties()
} catch (e) {
this.$eventHub.$emit('notification', {
text: 'Failed to get object properties from viewer.'
})
}
},
captureProgress(args) {
captureProgress(args: { progress: number }) {
this.loadProgress = args.progress * 100
},
captureSelect(selectionData) {
captureSelect(selectionData: { userData: Record<string, unknown>[] }) {
this.selectionData.splice(0, this.selectionData.length)
this.selectionData.push(...selectionData.userData)
}
}
}
})
</script>
@@ -107,6 +107,7 @@ import {
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import { useRoute } from '@/main/lib/core/composables/router'
import { AppLocalStorage } from '@/utils/localStorage'
export default {
name: 'TheBranch',
@@ -207,7 +208,7 @@ export default {
return this.$apollo.loading || this.streamLoading
},
loggedInUserId() {
return localStorage.getItem('uuid')
return AppLocalStorage.get('uuid')
},
streamId() {
return this.$route.params.streamId
@@ -145,6 +145,7 @@ import LeaveStreamPanel from '@/main/components/stream/collaborators/LeaveStream
import { IsLoggedInMixin } from '@/main/lib/core/mixins/isLoggedInMixin'
import { vueWithMixins } from '@/helpers/typeHelpers'
import { convertThrowIntoFetchResult } from '@/main/lib/common/apollo/helpers/apolloOperationHelper'
import { AppLocalStorage } from '@/utils/localStorage'
export default vueWithMixins(IsLoggedInMixin).extend({
// @vue/component
@@ -265,7 +266,7 @@ export default vueWithMixins(IsLoggedInMixin).extend({
return users
},
myId() {
return localStorage.getItem('uuid')
return AppLocalStorage.get('uuid')
}
},
mounted() {
@@ -68,6 +68,7 @@
v-if="c"
:comment="c"
:stream="stream"
:stream-id="$route.params.streamId"
@deleted="handleDeletion"
/>
</v-col>
@@ -96,6 +97,7 @@ import {
buildPortalStateMixin
} from '@/main/utils/portalStateManager'
import { COMMENT_FULL_INFO_FRAGMENT } from '@/graphql/comments'
import { setSelectedCommentMetaData } from '@/main/lib/viewer/commit-object-viewer/stateManager'
export default {
name: 'TheComments',
@@ -193,7 +195,7 @@ export default {
},
methods: {
handleDeletion(comment) {
this.$store.commit('setCommentSelection', { comment: null })
setSelectedCommentMetaData(null)
const indx = this.localComments.findIndex((lc) => lc.id === comment.id)
this.localComments.splice(indx, 1)
},
@@ -0,0 +1,318 @@
<template>
<v-app
:class="`embed-viewer no-scrollbar ${
transparent ? '' : $vuetify.theme.dark ? 'background-dark' : 'background-light'
}`"
>
<!-- BG image -->
<div
v-if="resourceMetadata && !isModelLoaded"
style="position: fixed; top: 0; width: 100%; height: 100%; cursor: pointer"
class="embed-bg"
@click="load()"
>
<preview-image :url="previewUrl" :height="height" rotate></preview-image>
</div>
<!-- Play button -->
<div
v-if="!isModelLoaded && !error"
class="viewer-play d-flex fullscreen align-center justify-center no-mouse"
>
<v-btn
id="viewer-play-btn"
:disabled="showPlayLoader"
fab
color="primary"
class="elevation-4 hover-tada mouse"
@click="load()"
>
<v-icon v-if="!showPlayLoader">mdi-play</v-icon>
<v-icon v-else class="spinning-icon">mdi-loading</v-icon>
</v-btn>
</div>
<!-- This should always be conditionally and asynchronously loaded so that heavy viewer deps are lazy loaded -->
<embedded-commit-object-viewer
v-if="shouldLoadHeavyDeps"
:stream-id="streamId"
:resource-id="resourceMetadata.resourceId"
@models-loaded="onModelsLoaded"
/>
<!-- Display error if needed -->
<div v-if="error" class="fullscreen d-flex justify-center align-center">
<div class="">
<p class="text-h5 text-center red--text">Speckle Embedding Error</p>
<p class="text-center grey--text">
Double check to see if the stream is public and if the embed link is correct.
</p>
</div>
</div>
</v-app>
</template>
<script lang="ts">
import { Nullable } from '@/helpers/typeHelpers'
import { computed, defineComponent, ref, onBeforeMount, watch, onMounted } from 'vue'
import { useRoute } from '@/main/lib/core/composables/router'
import { useMixpanel } from '@/main/lib/core/composables/tracking'
import { useApolloClient } from '@vue/apollo-composable'
import {
StreamBranchFirstCommitDocument,
StreamFirstCommitDocument
} from '@/graphql/generated/graphql'
import { useWindowSize } from '@vueuse/core'
import { isDarkTheme } from '@/main/utils/themeStateManager'
import PreviewImage from '@/main/components/common/PreviewImage.vue'
import { useEmbedViewerQuery } from '@/main/lib/viewer/commit-object-viewer/composables/embed'
export default defineComponent({
name: 'TheEmbed',
components: {
EmbeddedCommitObjectViewer: () =>
import('@/main/components/viewer/embed/EmbeddedCommitObjectViewer.vue'),
PreviewImage
},
setup() {
const route = useRoute()
const apollo = useApolloClient()
const mixpanel = useMixpanel()
const isInitialized = ref(false)
const isModelLoaded = ref(false)
const shouldLoadHeavyDeps = ref(false)
const error = ref(null as Nullable<Error>)
const resourceMetadata = ref(
null as Nullable<{
resourceId: string
objectId?: Nullable<string>
type: 'commit' | 'object'
}>
)
const displayType = computed(() => {
let type: string | null = null
if (route.query.stream) type = 'stream'
if (route.query.branch) type = 'branch'
if (route.query.commit) type = 'commit'
if (route.query.object) type = 'object'
return type
})
const { height } = useWindowSize()
const { streamId, branchName, commitId, objectId, transparent } =
useEmbedViewerQuery()
const previewUrl = computed(() => {
if (!resourceMetadata.value || !streamId.value) return null
if (resourceMetadata.value.objectId) {
return `/preview/${streamId.value}/objects/${resourceMetadata.value.objectId}`
} else if (resourceMetadata.value.type === 'commit') {
return `/preview/${streamId.value}/commits/${resourceMetadata.value.resourceId}`
} else {
return null
}
})
const showPlayLoader = computed(
() => !isInitialized.value || (shouldLoadHeavyDeps.value && !isModelLoaded.value)
)
onBeforeMount(async () => {
const type = displayType.value
try {
if (!streamId.value) {
throw new Error('Stream ID must be specified')
}
if (type === 'stream') {
// Resolve the stream's first commit
const res = await apollo.client.query({
query: StreamFirstCommitDocument,
variables: {
id: streamId.value
}
})
if (!res.data.stream?.commits?.items)
throw new Error('Could not resolve stream')
if (
res.data.stream.commits.totalCount === 0 ||
!res.data.stream.commits.items[0]
)
throw new Error('Stream has no commits.')
resourceMetadata.value = {
resourceId: res.data.stream.commits.items[0].id,
objectId: res.data.stream.commits.items[0].referencedObject,
type: 'commit'
}
} else if (type === 'branch' && branchName.value) {
// Resolve a stream branch's first commit
const res = await apollo.client.query({
query: StreamBranchFirstCommitDocument,
variables: {
id: streamId.value,
branch: branchName.value
}
})
if (!res.data.stream?.branch?.commits?.items)
throw new Error('Could not resolve stream or its branch')
if (
res.data.stream.branch.commits.totalCount === 0 ||
!res.data.stream.branch.commits.items[0]
)
throw new Error('Branch has no commits.')
resourceMetadata.value = {
resourceId: res.data.stream.branch.commits.items[0].id,
objectId: res.data.stream.branch.commits.items[0].referencedObject,
type: 'commit'
}
} else if (type === 'commit' && commitId.value) {
resourceMetadata.value = {
resourceId: commitId.value,
type: 'commit'
}
} else if (type === 'object' && objectId.value) {
resourceMetadata.value = {
resourceId: objectId.value,
objectId: objectId.value,
type: 'object'
}
}
if (!resourceMetadata.value) {
throw new Error('Unexpected display type or invalid parameters')
}
// Mark as ready for loading
isInitialized.value = true
} catch (err: unknown) {
error.value = err instanceof Error ? err : new Error('Unexpected error')
isInitialized.value = shouldLoadHeavyDeps.value = isModelLoaded.value = false
}
})
const updateTransparency = () => {
const classList = document.getElementById('app')!.classList
if (transparent.value) {
classList.remove('theme--dark')
classList.remove('theme--light')
} else {
const isDarkMode = isDarkTheme()
classList.add(`theme--${isDarkMode ? 'dark' : 'light'}`)
}
}
watch(() => transparent, updateTransparency)
onMounted(() => updateTransparency())
return {
displayType,
streamId,
branchName,
commitId,
objectId,
error,
resourceMetadata,
isInitialized,
isModelLoaded,
shouldLoadHeavyDeps,
height,
transparent,
previewUrl,
showPlayLoader,
onError: (e: unknown) => {
error.value = e instanceof Error ? e : new Error('Unexpected error')
},
onModelsLoaded: () => {
isModelLoaded.value = true
},
load: () => {
if (!isInitialized.value || shouldLoadHeavyDeps.value || isModelLoaded.value)
return
shouldLoadHeavyDeps.value = true
mixpanel.track('Embedded Model Load', {
type: 'action'
})
}
}
}
})
</script>
<style lang="scss">
body::-webkit-scrollbar {
display: none;
}
.fullscreen {
height: 100vh !important;
width: 100vw !important;
position: fixed;
&::-webkit-scrollbar {
display: none;
}
top: 0;
left: 0;
z-index: 10;
}
.no-scrollbar {
width: 100vw;
height: 100vh;
overflow: hidden;
&::-webkit-scrollbar {
display: none;
}
}
.bg-img {
background-position: center;
background-repeat: no-repeat;
/*background-attachment: fixed;*/
filter: blur(2px);
}
#viewer-play-btn {
@keyframes spinner-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
// kinda hacky, but vuetify renders the disabled state in a stupid way that relies
// on the background color of the thing behind the button
&.v-btn--fab.v-btn--disabled {
background-color: #242424 !important;
}
.spinning-icon {
animation: spinner-spin 0.5s linear infinite;
}
}
.no-mouse {
pointer-events: none;
}
.mouse {
pointer-events: auto;
}
</style>
@@ -82,7 +82,7 @@
:md="`${comments.items.length !== 1 ? '6' : '12'}`"
:xl="`${comments.items.length !== 1 ? '6' : '12'}`"
>
<comment-list-item :comment="c" :stream="stream" />
<comment-list-item :comment="c" :stream="stream" :stream-id="streamId" />
</v-col>
<v-col v-if="comments.totalCount === 0" class="mt-5">
<div class="d-flex align-center">
@@ -132,6 +132,7 @@
<script>
import { gql } from '@apollo/client/core'
import { COMMENT_FULL_INFO_FRAGMENT } from '@/graphql/comments'
import { AppLocalStorage } from '@/utils/localStorage'
export default {
name: 'TheStreamHome',
@@ -246,7 +247,7 @@ export default {
return 12
},
loggedIn() {
return localStorage.getItem('uuid') !== null
return AppLocalStorage.get('uuid') !== null
}
},
watch: {},
@@ -46,6 +46,7 @@ import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
} from '@/main/utils/portalStateManager'
import { AppLocalStorage } from '@/utils/localStorage'
export default {
name: 'TheProfileUser',
@@ -95,7 +96,7 @@ export default {
},
created() {
// Move to self profile
if (this.$route.params.userId === localStorage.getItem('uuid')) {
if (this.$route.params.userId === AppLocalStorage.get('uuid')) {
this.$router.replace({ path: '/profile' })
}
},
+29 -3
View File
@@ -148,7 +148,11 @@ const routes = [
resizableNavbar: true,
hideEmailBanner: true
},
component: () => import('@/main/pages/stream/CommitObjectViewer.vue')
component: () => import('@/main/pages/stream/CommitObjectViewer.vue'),
props: (route) => ({
streamId: route.params.streamId,
resourceId: route.params.resourceId
})
},
{
path: 'objects/:resourceId*',
@@ -158,7 +162,11 @@ const routes = [
resizableNavbar: true,
hideEmailBanner: true
},
component: () => import('@/main/pages/stream/CommitObjectViewer.vue')
component: () => import('@/main/pages/stream/CommitObjectViewer.vue'),
props: (route) => ({
streamId: route.params.streamId,
resourceId: route.params.resourceId
})
},
{
path: 'collaborators/',
@@ -293,6 +301,23 @@ const routes = [
title: 'Not Found | Speckle'
},
component: () => import('@/main/pages/common/NotFound.vue')
},
{
path: '/embed',
meta: {
title: 'Embed View | Speckle'
},
component: () => import('@/main/layouts/TheBasic.vue'),
children: [
{
path: '/',
name: 'viewer-embed',
meta: {
title: 'Embed View | Speckle'
},
component: () => import('@/main/pages/stream/TheEmbed.vue')
}
]
}
]
@@ -320,7 +345,8 @@ function shouldForceToLogin(isLoggedIn, to) {
'Register',
'Error',
'Reset Password',
'Reset Password Finalization'
'Reset Password Finalization',
'viewer-embed'
]
// Check if any of the new routes (nested or not) is one of the routes that is allowed for unauthed users
-283
View File
@@ -1,283 +0,0 @@
import Vue from 'vue'
import Vuex from 'vuex'
import emojis from './emojis'
Vue.use(Vuex)
// Note: this is currently used only for 3d viewer filtering purposes. All other state
// is handled by apollo. Ideally we would only add extra app state in here if really
// necessary (ie, component local state + events is not enough).
const store = new Vuex.Store({
state: {
viewerBusy: false,
appliedFilter: null,
isolateKey: null,
isolateValues: [],
hideKey: null,
hideValues: [],
colorLegend: {},
isolateCategoryKey: null,
isolateCategoryValues: [],
hideCategoryKey: null,
hideCategoryValues: [],
selectedComment: null,
addingComment: false,
preventCommentCollapse: false,
commentReactions: ['❤️', '✏️', '🔥', '⚠️'],
emojis
},
mutations: {
setViewerBusy(state, { viewerBusyState }) {
state.viewerBusy = viewerBusyState
},
setAddingCommentState(state, { addingCommentState }) {
state.addingComment = addingCommentState
},
setCommentSelection(state, { comment }) {
if (comment) window.__viewer.interactions.deselectObjects()
state.selectedComment = comment
},
isolateObjects(state, { filterKey, filterValues }) {
state.hideKey = null
state.hideValues = []
if (state.isolateKey !== filterKey) state.isolateValues = []
state.isolateKey = filterKey
state.isolateValues = [...new Set([...state.isolateValues, ...filterValues])]
if (state.isolateValues.length === 0) state.appliedFilter = null
else
state.appliedFilter = {
filterBy: { [filterKey]: { includes: state.isolateValues } },
ghostOthers: true
}
window.__viewer.applyFilter(state.appliedFilter)
},
unisolateObjects(state, { filterKey, filterValues }) {
state.hideKey = null
state.hideValues = []
if (state.isolateKey !== filterKey) state.isolateValues = []
state.isolateKey = filterKey
state.isolateValues = state.isolateValues.filter(
(val) => filterValues.indexOf(val) === -1
)
if (state.isolateValues.length === 0) state.appliedFilter = null
else
state.appliedFilter = {
filterBy: { [filterKey]: { includes: state.isolateValues } },
ghostOthers: true
}
window.__viewer.applyFilter(state.appliedFilter)
},
hideObjects(state, { filterKey, filterValues }) {
state.isolateKey = null
state.isolateValues = []
if (state.hideKey !== filterKey) state.hideValues = []
state.hideKey = filterKey
state.hideValues = [...new Set([...filterValues, ...state.hideValues])]
if (state.hideValues.length === 0) state.appliedFilter = null
else
state.appliedFilter = {
filterBy: { [filterKey]: { excludes: state.hideValues } }
}
window.__viewer.applyFilter(state.appliedFilter)
},
showObjects(state, { filterKey, filterValues }) {
state.isolateKey = null
state.isolateValues = []
if (state.hideKey !== filterKey) state.hideValues = []
state.hideKey = filterKey
state.hideValues = state.hideValues.filter(
(val) => filterValues.indexOf(val) === -1
)
if (state.hideValues.length === 0) state.appliedFilter = null
else
state.appliedFilter = {
filterBy: { [filterKey]: { excludes: state.hideValues } }
}
window.__viewer.applyFilter(state.appliedFilter)
},
async isolateCategoryToggle(
state,
{ filterKey, filterValue, allValues, colorBy = false }
) {
this.commit('resetInternalHideIsolateObjectState')
state.hideCategoryKey = null
state.hideCategoryValues = []
if (filterKey !== state.isolateCategoryKey) state.isolateCategoryValues = []
state.isolateCategoryKey = filterKey
const indx = state.isolateCategoryValues.indexOf(filterValue)
if (indx === -1) state.isolateCategoryValues.push(filterValue)
else state.isolateCategoryValues.splice(indx, 1)
if (
(state.isolateCategoryValues.length === 0 ||
state.isolateCategoryValues.length === allValues.length) &&
!colorBy
) {
state.appliedFilter = null
window.__viewer.applyFilter(state.appliedFilter)
return
}
if (state.isolateCategoryValues.length === 0 && colorBy) {
state.appliedFilter = {
colorBy: { type: 'category', property: filterKey }
}
}
if (state.isolateCategoryValues.length !== 0) {
state.appliedFilter = {
ghostOthers: true,
filterBy: { [filterKey]: state.isolateCategoryValues },
colorBy: colorBy ? { type: 'category', property: filterKey } : null
}
}
if (state.isolateCategoryValues.length === allValues.length)
delete state.appliedFilter.filterBy
const res = await window.__viewer.applyFilter(state.appliedFilter)
state.colorLegend = res.colorLegend
},
async hideCategoryToggle(state, { filterKey, filterValue, colorBy = false }) {
this.commit('resetInternalHideIsolateObjectState')
state.isolateCategoryKey = null
state.isolateCategoryValues = []
if (filterKey !== state.hideCategoryKey) state.hideCategoryValues = []
state.hideCategoryKey = filterKey
const indx = state.hideCategoryValues.indexOf(filterValue)
if (indx === -1) state.hideCategoryValues.push(filterValue)
else state.hideCategoryValues.splice(indx, 1)
if (state.hideCategoryValues.length === 0 && !colorBy) {
state.appliedFilter = null
window.__viewer.applyFilter(state.appliedFilter)
return
}
if (state.hideCategoryValues.length === 0 && colorBy) {
state.appliedFilter = {
colorBy: { type: 'category', property: filterKey }
}
}
if (state.hideCategoryValues.length !== 0) {
state.appliedFilter = {
filterBy: { [filterKey]: { not: state.hideCategoryValues } },
colorBy: colorBy ? { type: 'category', property: filterKey } : null
}
}
const res = await window.__viewer.applyFilter(state.appliedFilter)
state.colorLegend = res.colorLegend
},
async toggleColorByCategory(state, { filterKey }) {
if (state.appliedFilter && state.appliedFilter.colorBy) {
state.appliedFilter.colorBy = null
} else
state.appliedFilter = {
...state.appliedFilter,
colorBy: { type: 'category', property: filterKey }
}
const res = await window.__viewer.applyFilter(state.appliedFilter)
state.colorLegend = res.colorLegend
},
setNumericFilter(
state,
{ filterKey, minValue, maxValue, gradientColors = ['#3F5EFB', '#FC466B'] }
) {
this.commit('resetInternalHideIsolateObjectState')
this.commit('resetInternalCategoryObjectState')
state.appliedFilter = {
ghostOthers: true,
colorBy: {
type: 'gradient',
property: filterKey,
minValue,
maxValue,
gradientColors
},
filterBy: { [filterKey]: { gte: minValue, lte: maxValue } }
}
window.__viewer.applyFilter(state.appliedFilter)
},
async setFilterDirect(state, { filter }) {
const filterBy = filter.filterBy
if (filterBy && filterBy.__parents) {
if (filterBy.__parents.includes) {
this.commit('isolateObjects', {
filterKey: '__parents',
filterValues: filterBy.__parents.includes
})
return
}
if (filterBy.__parents.excludes) {
this.commit('hideObjects', {
filterKey: '__parents',
filterValues: filterBy.__parents.excludes
})
return
}
} else if (filter.ghostOthers) {
// means it's isolate by category or numeric filter
if (filter.colorBy && filter.colorBy.type === 'gradient') {
this.commit('setNumericFilter', {
filterKey: Object.keys(filter.filterBy)[0],
minValue: filter.filterBy[Object.keys(filter.filterBy)[0]].gte,
maxValue: filter.filterBy[Object.keys(filter.filterBy)[0]].lte
})
} else {
const values = filterBy[Object.keys(filter.filterBy)[0]]
for (const val of values) {
const f = {
filterKey: Object.keys(filter.filterBy)[0],
filterValue: val,
allValues: [],
colorBy: filter.colorBy
}
this.commit('isolateCategoryToggle', f)
}
}
} else if (filterBy) {
const values = filterBy[Object.keys(filter.filterBy)[0]].not
for (const val of values) {
const f = {
filterKey: Object.keys(filter.filterBy)[0],
filterValue: val,
allValues: [],
colorBy: filter.colorBy
}
this.commit('hideCategoryToggle', f)
}
} else if (filter.colorBy) {
this.commit('toggleColorByCategory', { filterKey: filter.colorBy.property })
}
},
resetInternalHideIsolateObjectState(state) {
state.isolateKey = null
state.isolateValues = []
state.hideKey = null
state.hideValues = []
},
resetInternalCategoryObjectState(state) {
state.isolateCategoryKey = null
state.isolateCategoryValues = []
state.hideCategoryKey = null
state.hideCategoryValues = []
},
resetFilter(state) {
this.commit('resetInternalHideIsolateObjectState')
this.commit('resetInternalCategoryObjectState')
state.appliedFilter = null
state.preventCommentCollapse = true
window.__viewer.applyFilter(state.appliedFilter)
},
setPreventCommentCollapse(state, { value }) {
state.preventCommentCollapse = value
}
}
})
export default store
@@ -1,3 +1,4 @@
import { AppLocalStorage } from '@/utils/localStorage'
import Vue from 'vue'
/**
@@ -16,11 +17,7 @@ export function setDarkTheme(val, save = false) {
themeState.dark = !!val
if (!save) return
try {
localStorage.setItem(LOCAL_STORAGE_KEY, val ? THEME_DARK : THEME_LIGHT)
} catch (e) {
// Suppressing missing localStorage errors
}
AppLocalStorage.set(LOCAL_STORAGE_KEY, val ? THEME_DARK : THEME_LIGHT)
}
export function isDarkTheme() {
@@ -28,14 +25,10 @@ export function isDarkTheme() {
}
export function initialize() {
try {
const storageSetting = localStorage.getItem(LOCAL_STORAGE_KEY)
if (storageSetting) {
setDarkTheme(storageSetting === THEME_DARK)
return
}
} catch (e) {
// Suppressing missing localStorage errors
const storageSetting = AppLocalStorage.get(LOCAL_STORAGE_KEY)
if (storageSetting) {
setDarkTheme(storageSetting === THEME_DARK)
return
}
const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)').matches
+22 -27
View File
@@ -2,18 +2,13 @@ import { mainUserDataQuery } from '@/graphql/user'
import { LocalStorageKeys } from '@/helpers/mainConstants'
import md5 from '@/helpers/md5'
import { VALID_EMAIL_REGEX } from '@/main/lib/common/vuetify/validators'
import { AppLocalStorage } from '@/utils/localStorage'
const appId = 'spklwebapp'
const appSecret = 'spklwebapp'
export function getAuthToken() {
try {
return localStorage.getItem(LocalStorageKeys.AuthToken)
} catch (e) {
// suppressed localStorage errors
}
return null
return AppLocalStorage.get(LocalStorageKeys.AuthToken)
}
/**
@@ -26,8 +21,8 @@ export async function checkAccessCodeAndGetTokens() {
const response = await getTokenFromAccessCode(accessCode)
// eslint-disable-next-line no-prototype-builtins
if (response.hasOwnProperty('token')) {
localStorage.setItem(LocalStorageKeys.AuthToken, response.token)
localStorage.setItem(LocalStorageKeys.RefreshToken, response.refreshToken)
AppLocalStorage.set(LocalStorageKeys.AuthToken, response.token)
AppLocalStorage.set(LocalStorageKeys.RefreshToken, response.refreshToken)
return true
}
} else {
@@ -41,7 +36,7 @@ export async function checkAccessCodeAndGetTokens() {
* @return {Object} The full graphql response.
*/
export async function prefetchUserAndSetSuuid(apolloClient) {
const token = localStorage.getItem(LocalStorageKeys.AuthToken)
const token = AppLocalStorage.get(LocalStorageKeys.AuthToken)
if (!token) return
// Pull user info (& remember it in the Apollo cache)
@@ -53,10 +48,10 @@ export async function prefetchUserAndSetSuuid(apolloClient) {
// eslint-disable-next-line camelcase
const distinct_id = '@' + md5(data.user.email.toLowerCase()).toUpperCase()
localStorage.setItem('suuid', data.user.suuid)
localStorage.setItem('distinct_id', distinct_id)
localStorage.setItem('uuid', data.user.id)
localStorage.setItem('stcount', data.user.streams.totalCount)
AppLocalStorage.set('suuid', data.user.suuid)
AppLocalStorage.set('distinct_id', distinct_id)
AppLocalStorage.set('uuid', data.user.id)
AppLocalStorage.set('stcount', data.user.streams.totalCount)
return data
} else {
await signOut()
@@ -74,7 +69,7 @@ export async function getTokenFromAccessCode(accessCode) {
accessCode,
appId,
appSecret,
challenge: localStorage.getItem('appChallenge')
challenge: AppLocalStorage.get('appChallenge')
})
})
@@ -93,18 +88,18 @@ export async function signOut(mixpanelInstance) {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: localStorage.getItem(LocalStorageKeys.AuthToken),
refreshToken: localStorage.getItem(LocalStorageKeys.RefreshToken)
token: AppLocalStorage.get(LocalStorageKeys.AuthToken),
refreshToken: AppLocalStorage.get(LocalStorageKeys.RefreshToken)
})
})
localStorage.removeItem(LocalStorageKeys.AuthToken)
localStorage.removeItem(LocalStorageKeys.RefreshToken)
localStorage.removeItem('suuid')
localStorage.removeItem('uuid')
localStorage.removeItem('distinct_id')
localStorage.removeItem('stcount')
localStorage.removeItem('onboarding')
AppLocalStorage.remove(LocalStorageKeys.AuthToken)
AppLocalStorage.remove(LocalStorageKeys.RefreshToken)
AppLocalStorage.remove('suuid')
AppLocalStorage.remove('uuid')
AppLocalStorage.remove('distinct_id')
AppLocalStorage.remove('stcount')
AppLocalStorage.remove('onboarding')
window.location = '/'
@@ -115,7 +110,7 @@ export async function signOut(mixpanelInstance) {
}
export async function refreshToken() {
const refreshToken = localStorage.getItem(LocalStorageKeys.RefreshToken)
const refreshToken = AppLocalStorage.get(LocalStorageKeys.RefreshToken)
if (!refreshToken) throw new Error('No refresh token found')
const refreshResponse = await fetch('/auth/token', {
@@ -134,8 +129,8 @@ export async function refreshToken() {
// eslint-disable-next-line no-prototype-builtins
if (data.hasOwnProperty('token')) {
localStorage.setItem(LocalStorageKeys.AuthToken, data.token)
localStorage.setItem(LocalStorageKeys.RefreshToken, data.refreshToken)
AppLocalStorage.set(LocalStorageKeys.AuthToken, data.token)
AppLocalStorage.set(LocalStorageKeys.RefreshToken, data.refreshToken)
await prefetchUserAndSetSuuid()
return true
}
@@ -1,3 +0,0 @@
export function resourceType(resourceId) {
return resourceId.length === 10 ? 'commit' : 'object'
}
+17
View File
@@ -250,3 +250,20 @@ html {
scrollbar-color: $sb-color;
}
}
// v-navigation-drawer resizer global restyle
.nav-resizer {
position: absolute;
top: 0;
right: 0;
width: 0px;
height: 100%;
z-index: 100000;
transition: all 0.6s ease;
opacity: 0.01;
border: 4px solid royalblue;
}
.nav-resizer:hover {
opacity: 0.5;
width: 0px;
}
@@ -0,0 +1,6 @@
declare module '*.gql' {
import { DocumentNode } from '@apollo/client/core'
const operation: DocumentNode
export default operation
}
+1 -1
View File
@@ -23,7 +23,7 @@ declare module 'vue/types/vue' {
/**
* Check if auth token is stored in localStorage
* @deprecated Use `isLoggedInQuery`/`isLoggedInMixin` instead
* @deprecated Use `isLoggedInQuery`/`isLoggedInMixin`/`useIsLoggedIn` instead
*/
$loggedIn: () => boolean
}
+2 -2
View File
@@ -3,8 +3,8 @@ export {}
declare global {
interface Window {
/**
* Initialized in SpeckleViewer.vue
* Used in some layout components
*/
__viewer?: import('@speckle/viewer').Viewer
__lastNavSize?: number
}
}
+8 -7
View File
@@ -28,6 +28,14 @@ const config = {
// Setting source map according to build env
config.devtool(isProdBuild ? false : 'eval-source-map')
// Enable .mjs support
config.module
.rule('mjs-support')
.test(/\.mjs$/)
.type('javascript/auto')
.include.add(/node_modules/)
.end()
},
productionSourceMap: false,
pages: {
@@ -36,12 +44,6 @@ const config = {
title: 'Speckle',
template: 'public/app.html',
filename: 'app.html'
},
embedApp: {
entry: 'src/embed/embedApp.js',
title: 'Speckle Embed Viewer',
template: 'public/embedApp.html',
filename: 'embedApp.html'
}
},
devServer: {
@@ -50,7 +52,6 @@ const config = {
historyApiFallback: {
rewrites: [
{ from: /^\/$/, to: '/app.html' },
{ from: /\/embed(.*)/, to: '/embedApp.html' },
{ from: /./, to: '/app.html' }
]
}
+4 -5
View File
@@ -303,12 +303,11 @@ export class Viewer extends EventEmitter implements IViewer {
}
public async unloadAll() {
for (const key of Object.keys(this.loaders)) {
await this.loaders[key].unload()
delete this.loaders[key]
}
const loaders = Object.values(this.loaders)
this.loaders = {}
await Promise.all(loaders.map((l) => l.unload()))
await this.applyFilter(null)
return
}
public async applyFilter(filter: unknown) {
+52 -20
View File
@@ -4855,11 +4855,11 @@ __metadata:
"@vue/cli-plugin-babel": ~4.3.1
"@vue/cli-plugin-router": ~4.3.1
"@vue/cli-plugin-typescript": ~4.5.19
"@vue/cli-plugin-vuex": ~4.3.1
"@vue/cli-service": ~4.3.1
"@vue/eslint-config-typescript": ^11.0.0
"@vuejs-community/vue-filter-date-format": ^1.6.3
"@vuejs-community/vue-filter-date-parse": ^1.1.6
"@vueuse/core": ^9.0.0
apexcharts: ^3.33.1
apollo-upload-client: ^17.0.0
babel-eslint: ^10.1.0
@@ -4903,7 +4903,6 @@ __metadata:
vuetify: ^2.3.21
vuetify-image-input: ^19.1.0
vuetify-loader: ^1.9.1
vuex: ^3.6.2
webpack: ^4.46.0
webpack-bundle-analyzer: ^4.5.0
languageName: unknown
@@ -6011,6 +6010,13 @@ __metadata:
languageName: node
linkType: hard
"@types/web-bluetooth@npm:^0.0.15":
version: 0.0.15
resolution: "@types/web-bluetooth@npm:0.0.15"
checksum: 4e3b3b1c0baf6735690ce0c5ffaac53de3dbd16362316cbc5e32970bcb1e1baf16dae0a9f30fe86256ad0ee22a4533423f443835273efc54b15235086ebda85b
languageName: node
linkType: hard
"@types/webpack-env@npm:^1.15.2":
version: 1.17.0
resolution: "@types/webpack-env@npm:1.17.0"
@@ -6592,15 +6598,6 @@ __metadata:
languageName: node
linkType: hard
"@vue/cli-plugin-vuex@npm:~4.3.1":
version: 4.3.1
resolution: "@vue/cli-plugin-vuex@npm:4.3.1"
peerDependencies:
"@vue/cli-service": ^3.0.0 || ^4.0.0-0
checksum: 0de3d1b4798793273527ffe6435a070c13376b058b009c6dbc53fa23eda64ac1a8ffed9f097a5334e863cb7dbd782d4112b918c30df34988e0c5ce916e3fa571
languageName: node
linkType: hard
"@vue/cli-service@npm:~4.3.1":
version: 4.3.1
resolution: "@vue/cli-service@npm:4.3.1"
@@ -6926,6 +6923,34 @@ __metadata:
languageName: node
linkType: hard
"@vueuse/core@npm:^9.0.0":
version: 9.0.0
resolution: "@vueuse/core@npm:9.0.0"
dependencies:
"@types/web-bluetooth": ^0.0.15
"@vueuse/metadata": 9.0.0
"@vueuse/shared": 9.0.0
vue-demi: "*"
checksum: 2825bad3f90bf030a23e4f14d67018a9b3497e672fa61fc555da483488bb4df28d61e2141637d6ed97a3d0c8544f60e7474ebf94d7deb6933199727505cba08b
languageName: node
linkType: hard
"@vueuse/metadata@npm:9.0.0":
version: 9.0.0
resolution: "@vueuse/metadata@npm:9.0.0"
checksum: 13404823d085df4d5221a7be4c8847915d7682888d9465aad3b257e511a509e1baf99575236cdfd2d3cc1abaaddf4180ab5e6e63fb54d6dfbf007c099e0020b3
languageName: node
linkType: hard
"@vueuse/shared@npm:9.0.0":
version: 9.0.0
resolution: "@vueuse/shared@npm:9.0.0"
dependencies:
vue-demi: "*"
checksum: 4925d94e53d27174757e153ac3fc1f350633399faa2a8f4e6cf323b5ccb0d106e2415e050664c3fe325932464bfaaad7ea9d38afb2ffa219e35f6accd64b2ee6
languageName: node
linkType: hard
"@webassemblyjs/ast@npm:1.11.1":
version: 1.11.1
resolution: "@webassemblyjs/ast@npm:1.11.1"
@@ -26774,6 +26799,22 @@ __metadata:
languageName: node
linkType: hard
"vue-demi@npm:*":
version: 0.13.6
resolution: "vue-demi@npm:0.13.6"
peerDependencies:
"@vue/composition-api": ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
"@vue/composition-api":
optional: true
bin:
vue-demi-fix: bin/vue-demi-fix.js
vue-demi-switch: bin/vue-demi-switch.js
checksum: 7712b1f8edad902a8ea99ac307f9dc276ca13cb4394fdc6753c27c32f16e5f4a6d3acaa752bf24d73be0ea972552ad7da4c61314b046fbc248bd9aa26afe2e59
languageName: node
linkType: hard
"vue-demi@npm:^0.13.1":
version: 0.13.5
resolution: "vue-demi@npm:0.13.5"
@@ -27028,15 +27069,6 @@ __metadata:
languageName: node
linkType: hard
"vuex@npm:^3.6.2":
version: 3.6.2
resolution: "vuex@npm:3.6.2"
peerDependencies:
vue: ^2.0.0
checksum: 37915741ba9557024ea42579e2c8d81847ec699d07d8ea3aa673929f26fceba8c59e7e1e6a875fd3962e50018bc30f782f5ace34d83307ef228ad46a4df343da
languageName: node
linkType: hard
"w3c-keyname@npm:^2.2.0":
version: 2.2.4
resolution: "w3c-keyname@npm:2.2.4"