chore: re-formatted everything with prettier

This commit is contained in:
Fabians
2022-03-29 16:53:49 +03:00
parent 4e97360e48
commit abe6d5e086
307 changed files with 12690 additions and 8378 deletions
+10 -8
View File
@@ -4,7 +4,6 @@ about: Help improve Speckle!
title: '' title: ''
labels: bug labels: bug
assignees: '' assignees: ''
--- ---
**What package are you referring to?** **What package are you referring to?**
@@ -15,6 +14,7 @@ A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
@@ -30,15 +30,17 @@ If applicable, add screenshots to help explain your problem.
If applicable, please fill in the below details - they help a lot! If applicable, please fill in the below details - they help a lot!
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari] - OS: [e.g. iOS]
- Version [e.g. 22] - Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):** **Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1] - Device: [e.g. iPhone6]
- Browser [e.g. stock browser, safari] - OS: [e.g. iOS8.1]
- Version [e.g. 22] - Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.
@@ -4,7 +4,6 @@ about: Suggest an idea for Speckle!
title: '' title: ''
labels: enhancement, question labels: enhancement, question
assignees: '' assignees: ''
--- ---
**What package are you referring to?** **What package are you referring to?**
-1
View File
@@ -75,4 +75,3 @@ jobs:
} }
} }
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }} }' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
+11
View File
@@ -0,0 +1,11 @@
node_modules
build
dist
coverage
.nyc_output
packages/server/reports*
package-lock.json
yarn.lock
# Prettier doesn't understand the syntax inside the Yaml files, because of the brackets
utils/helm/speckle-server/templates
+1 -3
View File
@@ -4,10 +4,8 @@
"semi": false, "semi": false,
"endOfLine": "auto", "endOfLine": "auto",
"bracketSpacing": true, "bracketSpacing": true,
"eslintIntegration": true,
"jsxBracketSameLine": true,
"vueIndentScriptAndStyle": false, "vueIndentScriptAndStyle": false,
"htmlWhitespaceSensitivity": "ignore", "htmlWhitespaceSensitivity": "ignore",
"printWidth": 100, "printWidth": 88,
"singleQuote": true "singleQuote": true
} }
+29 -30
View File
@@ -1,6 +1,5 @@
version: "2" version: '2'
services: services:
speckle-frontend: speckle-frontend:
build: build:
context: . context: .
@@ -8,7 +7,7 @@ services:
image: speckle/speckle-frontend:local image: speckle/speckle-frontend:local
restart: always restart: always
ports: ports:
- "0.0.0.0:80:80" - '0.0.0.0:80:80'
speckle-server: speckle-server:
build: build:
@@ -18,26 +17,26 @@ services:
restart: always restart: always
environment: environment:
# TODO: Change this to the URL of the speckle server, as accessed from the network # TODO: Change this to the URL of the speckle server, as accessed from the network
CANONICAL_URL: "http://localhost" CANONICAL_URL: 'http://localhost'
# TODO: Change this to a unique secret for this server # TODO: Change this to a unique secret for this server
SESSION_SECRET: "TODO:Replace" SESSION_SECRET: 'TODO:Replace'
STRATEGY_LOCAL: "true" STRATEGY_LOCAL: 'true'
DEBUG: "speckle:*" DEBUG: 'speckle:*'
POSTGRES_URL: "postgres" POSTGRES_URL: 'postgres'
POSTGRES_USER: "speckle" POSTGRES_USER: 'speckle'
POSTGRES_PASSWORD: "speckle" POSTGRES_PASSWORD: 'speckle'
POSTGRES_DB: "speckle" POSTGRES_DB: 'speckle'
REDIS_URL: "redis://redis" REDIS_URL: 'redis://redis'
S3_ENDPOINT: "http://minio:9000" S3_ENDPOINT: 'http://minio:9000'
S3_ACCESS_KEY: "minioadmin" S3_ACCESS_KEY: 'minioadmin'
S3_SECRET_KEY: "minioadmin" S3_SECRET_KEY: 'minioadmin'
S3_BUCKET: "speckle-server" S3_BUCKET: 'speckle-server'
S3_CREATE_BUCKET: "true" S3_CREATE_BUCKET: 'true'
preview-service: preview-service:
build: build:
@@ -45,11 +44,11 @@ services:
dockerfile: packages/preview-service/Dockerfile dockerfile: packages/preview-service/Dockerfile
image: speckle/speckle-preview-service:local image: speckle/speckle-preview-service:local
restart: always restart: always
mem_limit: "3000m" mem_limit: '3000m'
memswap_limit: "3000m" memswap_limit: '3000m'
environment: environment:
DEBUG: "preview-service:*" DEBUG: 'preview-service:*'
PG_CONNECTION_STRING: "postgres://speckle:speckle@postgres/speckle" PG_CONNECTION_STRING: 'postgres://speckle:speckle@postgres/speckle'
webhook-service: webhook-service:
build: build:
@@ -58,8 +57,8 @@ services:
image: speckle/speckle-webhook-service:local image: speckle/speckle-webhook-service:local
restart: always restart: always
environment: environment:
DEBUG: "webhook-service:*" DEBUG: 'webhook-service:*'
PG_CONNECTION_STRING: "postgres://speckle:speckle@postgres/speckle" PG_CONNECTION_STRING: 'postgres://speckle:speckle@postgres/speckle'
fileimport-service: fileimport-service:
build: build:
@@ -68,12 +67,12 @@ services:
image: speckle/speckle-fileimport-service:local image: speckle/speckle-fileimport-service:local
restart: always restart: always
environment: environment:
DEBUG: "fileimport-service:*" DEBUG: 'fileimport-service:*'
PG_CONNECTION_STRING: "postgres://speckle:speckle@postgres/speckle" PG_CONNECTION_STRING: 'postgres://speckle:speckle@postgres/speckle'
S3_ENDPOINT: "http://minio:9000" S3_ENDPOINT: 'http://minio:9000'
S3_ACCESS_KEY: "minioadmin" S3_ACCESS_KEY: 'minioadmin'
S3_SECRET_KEY: "minioadmin" S3_SECRET_KEY: 'minioadmin'
S3_BUCKET: "speckle-server" S3_BUCKET: 'speckle-server'
SPECKLE_SERVER_URL: "http://speckle-server:3000" SPECKLE_SERVER_URL: 'http://speckle-server:3000'
+1 -3
View File
@@ -1,6 +1,4 @@
{ {
"packages": [ "packages": ["packages/*"],
"packages/*"
],
"version": "independent" "version": "independent"
} }
+2
View File
@@ -3,6 +3,8 @@
"private": true, "private": true,
"scripts": { "scripts": {
"lint": "eslint . --ext .js,.ts,.vue", "lint": "eslint . --ext .js,.ts,.vue",
"prettier:check": "prettier --check .",
"prettier:fix": "prettier --write .",
"docker:deps:up": "docker-compose -f ./docker-compose-deps.yml up -d", "docker:deps:up": "docker-compose -f ./docker-compose-deps.yml up -d",
"docker:deps:down": "docker-compose -f ./docker-compose-deps.yml down", "docker:deps:down": "docker-compose -f ./docker-compose-deps.yml down",
"dev": "lerna run dev --parallel", "dev": "lerna run dev --parallel",
+74 -65
View File
@@ -1,49 +1,52 @@
'use strict' 'use strict'
const crypto = require( 'crypto' ) const crypto = require('crypto')
const crs = require( 'crypto-random-string' ) const crs = require('crypto-random-string')
const bcrypt = require( 'bcrypt' ) const bcrypt = require('bcrypt')
const knex = require( '../knex' ) const knex = require('../knex')
const Streams = ( ) => knex( 'streams' ) const Streams = () => knex('streams')
const Branches = ( ) => knex( 'branches' ) const Branches = () => knex('branches')
const Objects = ( ) => knex( 'objects' ) const Objects = () => knex('objects')
const Closures = ( ) => knex( 'object_children_closure' ) const Closures = () => knex('object_children_closure')
const ApiTokens = ( ) => knex( 'api_tokens' ) const ApiTokens = () => knex('api_tokens')
const TokenScopes = ( ) => knex( 'token_scopes' ) const TokenScopes = () => knex('token_scopes')
module.exports = class ServerAPI { module.exports = class ServerAPI {
constructor({ streamId }) {
constructor( { streamId } ) {
this.streamId = streamId this.streamId = streamId
this.isSending = false this.isSending = false
this.buffer = [] this.buffer = []
} }
async saveObject( obj ) { async saveObject(obj) {
if( !obj ) throw new Error( 'Null object' ) if (!obj) throw new Error('Null object')
if( !obj.id ) { if (!obj.id) {
obj.id = crypto.createHash( 'md5' ).update( JSON.stringify( obj ) ).digest( 'hex' ) obj.id = crypto.createHash('md5').update(JSON.stringify(obj)).digest('hex')
} }
await this.createObject( this.streamId, obj ) await this.createObject(this.streamId, obj)
return obj.id return obj.id
} }
async createObject( streamId, object ) { async createObject(streamId, object) {
let insertionObject = this.prepInsertionObject( streamId, object ) let insertionObject = this.prepInsertionObject(streamId, object)
let closures = [ ] let closures = []
let totalChildrenCountByDepth = {} let totalChildrenCountByDepth = {}
if ( object.__closure !== null ) { if (object.__closure !== null) {
for ( const prop in object.__closure ) { for (const prop in object.__closure) {
closures.push( { streamId: streamId, parent: insertionObject.id, child: prop, minDepth: object.__closure[ prop ] } ) closures.push({
streamId: streamId,
parent: insertionObject.id,
child: prop,
minDepth: object.__closure[prop]
})
if ( totalChildrenCountByDepth[ object.__closure[ prop ].toString( ) ] ) if (totalChildrenCountByDepth[object.__closure[prop].toString()])
totalChildrenCountByDepth[ object.__closure[ prop ].toString( ) ]++ totalChildrenCountByDepth[object.__closure[prop].toString()]++
else else totalChildrenCountByDepth[object.__closure[prop].toString()] = 1
totalChildrenCountByDepth[ object.__closure[ prop ].toString( ) ] = 1
} }
} }
@@ -51,30 +54,34 @@ module.exports = class ServerAPI {
delete insertionObject.__closure delete insertionObject.__closure
insertionObject.totalChildrenCount = closures.length insertionObject.totalChildrenCount = closures.length
insertionObject.totalChildrenCountByDepth = JSON.stringify( totalChildrenCountByDepth ) insertionObject.totalChildrenCountByDepth = JSON.stringify(
totalChildrenCountByDepth
)
let q1 = Objects( ).insert( insertionObject ).toString( ) + ' on conflict do nothing' let q1 = Objects().insert(insertionObject).toString() + ' on conflict do nothing'
await knex.raw( q1 ) await knex.raw(q1)
if ( closures.length > 0 ) { if (closures.length > 0) {
let q2 = `${ Closures().insert( closures ).toString() } on conflict do nothing` let q2 = `${Closures().insert(closures).toString()} on conflict do nothing`
await knex.raw( q2 ) await knex.raw(q2)
} }
return insertionObject.id return insertionObject.id
} }
prepInsertionObject( streamId, obj ) { prepInsertionObject(streamId, obj) {
const MAX_OBJECT_SIZE = 10 * 1024 * 1024 const MAX_OBJECT_SIZE = 10 * 1024 * 1024
if ( obj.hash ) if (obj.hash) obj.id = obj.hash
obj.id = obj.hash
else else
obj.id = obj.id || crypto.createHash( 'md5' ).update( JSON.stringify( obj ) ).digest( 'hex' ) // generate a hash if none is present obj.id =
obj.id || crypto.createHash('md5').update(JSON.stringify(obj)).digest('hex') // generate a hash if none is present
let stringifiedObj = JSON.stringify( obj ) let stringifiedObj = JSON.stringify(obj)
if ( stringifiedObj.length > MAX_OBJECT_SIZE ) { if (stringifiedObj.length > MAX_OBJECT_SIZE) {
throw new Error( `Object too large (${stringifiedObj.length} > ${MAX_OBJECT_SIZE})` ) throw new Error(
`Object too large (${stringifiedObj.length} > ${MAX_OBJECT_SIZE})`
)
} }
return { return {
@@ -85,41 +92,44 @@ module.exports = class ServerAPI {
} }
} }
async getBranchByNameAndStreamId({ streamId, name }) {
async getBranchByNameAndStreamId( { streamId, name } ) { let query = Branches()
let query = Branches( ).select( '*' ).where( { streamId: streamId } ).andWhere( knex.raw( 'LOWER(name) = ?', [ name ] ) ).first( ) .select('*')
.where({ streamId: streamId })
.andWhere(knex.raw('LOWER(name) = ?', [name]))
.first()
return await query return await query
} }
async createBranch( { name, description, streamId, authorId } ) { async createBranch({ name, description, streamId, authorId }) {
let branch = {} let branch = {}
branch.id = crs( { length: 10 } ) branch.id = crs({ length: 10 })
branch.streamId = streamId branch.streamId = streamId
branch.authorId = authorId branch.authorId = authorId
branch.name = name.toLowerCase( ) branch.name = name.toLowerCase()
branch.description = description branch.description = description
await Branches( ).returning( 'id' ).insert( branch ) await Branches().returning('id').insert(branch)
// update stream updated at // update stream updated at
await Streams().where( { id: streamId } ).update( { updatedAt: knex.fn.now() } ) await Streams().where({ id: streamId }).update({ updatedAt: knex.fn.now() })
return branch.id return branch.id
} }
async createBareToken( ) { async createBareToken() {
let tokenId = crs( { length: 10 } ) let tokenId = crs({ length: 10 })
let tokenString = crs( { length: 32 } ) let tokenString = crs({ length: 32 })
let tokenHash = await bcrypt.hash( tokenString, 10 ) let tokenHash = await bcrypt.hash(tokenString, 10)
let lastChars = tokenString.slice( tokenString.length - 6, tokenString.length ) let lastChars = tokenString.slice(tokenString.length - 6, tokenString.length)
return { tokenId, tokenString, tokenHash, lastChars } return { tokenId, tokenString, tokenHash, lastChars }
} }
async createToken( { userId, name, scopes, lifespan } ) { async createToken({ userId, name, scopes, lifespan }) {
let { tokenId, tokenString, tokenHash, lastChars } = await this.createBareToken( ) let { tokenId, tokenString, tokenHash, lastChars } = await this.createBareToken()
if ( scopes.length === 0 ) throw new Error( 'No scopes provided' ) if (scopes.length === 0) throw new Error('No scopes provided')
let token = { let token = {
id: tokenId, id: tokenId,
@@ -129,21 +139,20 @@ module.exports = class ServerAPI {
name: name, name: name,
lifespan: lifespan lifespan: lifespan
} }
let tokenScopes = scopes.map( scope => ( { tokenId: tokenId, scopeName: scope } ) ) let tokenScopes = scopes.map((scope) => ({ tokenId: tokenId, scopeName: scope }))
await ApiTokens( ).insert( token ) await ApiTokens().insert(token)
await TokenScopes( ).insert( tokenScopes ) await TokenScopes().insert(tokenScopes)
return { id: tokenId, token: tokenId + tokenString } return { id: tokenId, token: tokenId + tokenString }
} }
async revokeTokenById( tokenId ) { async revokeTokenById(tokenId) {
let delCount = await ApiTokens( ).where( { id: tokenId.slice( 0, 10 ) } ).del( ) let delCount = await ApiTokens()
.where({ id: tokenId.slice(0, 10) })
.del()
if ( delCount === 0 ) if (delCount === 0) throw new Error('Token revokation failed')
throw new Error( 'Token revokation failed' )
return true return true
} }
} }
+11 -11
View File
@@ -1,17 +1,17 @@
const fs = require( 'fs' ) const fs = require('fs')
const TMP_RESULTS_PATH = '/tmp/import_result.json' const TMP_RESULTS_PATH = '/tmp/import_result.json'
const { parseAndCreateCommit } = require( './index' ) const { parseAndCreateCommit } = require('./index')
async function main() { async function main() {
let cmdArgs = process.argv.slice( 2 ) let cmdArgs = process.argv.slice(2)
let [ filePath, userId, streamId, branchName, commitMessage ] = cmdArgs let [filePath, userId, streamId, branchName, commitMessage] = cmdArgs
console.log( 'ARGV: ', filePath, userId, streamId, branchName, commitMessage ) console.log('ARGV: ', filePath, userId, streamId, branchName, commitMessage)
const data = fs.readFileSync( filePath ) const data = fs.readFileSync(filePath)
let ifcInput = { let ifcInput = {
data, data,
@@ -19,7 +19,7 @@ async function main() {
userId: userId, userId: userId,
message: commitMessage || 'Imported file' message: commitMessage || 'Imported file'
} }
if ( branchName ) ifcInput.branchName = branchName if (branchName) ifcInput.branchName = branchName
let output = { let output = {
success: false, success: false,
@@ -27,21 +27,21 @@ async function main() {
} }
try { try {
let commitId = await parseAndCreateCommit( ifcInput ) let commitId = await parseAndCreateCommit(ifcInput)
output = { output = {
success: true, success: true,
commitId commitId
} }
} catch ( err ) { } catch (err) {
output = { output = {
success: false, success: false,
error: err.toString() error: err.toString()
} }
} }
fs.writeFileSync( TMP_RESULTS_PATH, JSON.stringify( output ) ) fs.writeFileSync(TMP_RESULTS_PATH, JSON.stringify(output))
process.exit( 0 ) process.exit(0)
} }
main() main()
+4 -1
View File
@@ -23,7 +23,10 @@ async function parseAndCreateCommit({
totalChildrenCount: tCount totalChildrenCount: tCount
} }
let branch = await serverApi.getBranchByNameAndStreamId({ streamId: streamId, name: branchName }) let branch = await serverApi.getBranchByNameAndStreamId({
streamId: streamId,
name: branchName
})
if (!branch) { if (!branch) {
await serverApi.createBranch({ await serverApi.createBranch({
+221 -108
View File
@@ -1,21 +1,23 @@
const WebIFC = require( 'web-ifc/web-ifc-api-node' ) const WebIFC = require('web-ifc/web-ifc-api-node')
const ServerAPI = require( './api.js' ) const ServerAPI = require('./api.js')
module.exports = class IFCParser { module.exports = class IFCParser {
constructor({ serverApi }) {
constructor( { serverApi } ) {
this.api = new WebIFC.IfcAPI() this.api = new WebIFC.IfcAPI()
this.serverApi = serverApi || new ServerAPI() this.serverApi = serverApi || new ServerAPI()
} }
async parse( data ) { async parse(data) {
if ( this.api.wasmModule === undefined ) await this.api.Init() if (this.api.wasmModule === undefined) await this.api.Init()
this.modelId = this.api.OpenModel( data, { COORDINATE_TO_ORIGIN: true, USE_FAST_BOOLS: true } ) this.modelId = this.api.OpenModel(data, {
COORDINATE_TO_ORIGIN: true,
USE_FAST_BOOLS: true
})
this.projectId = this.api.GetLineIDsWithType( this.modelId, WebIFC.IFCPROJECT ).get( 0 ) this.projectId = this.api.GetLineIDsWithType(this.modelId, WebIFC.IFCPROJECT).get(0)
this.project = this.api.GetLine( this.modelId, this.projectId, true ) this.project = this.api.GetLine(this.modelId, this.projectId, true)
this.project.__closure = {} this.project.__closure = {}
this.cache = {} this.cache = {}
@@ -25,51 +27,56 @@ module.exports = class IFCParser {
// as reference objects in this.productGeo // as reference objects in this.productGeo
this.productGeo = {} this.productGeo = {}
await this.createGeometries() await this.createGeometries()
console.log( `Geometries created: ${Object.keys( this.productGeo ).length} meshes.` ) console.log(`Geometries created: ${Object.keys(this.productGeo).length} meshes.`)
// Lastly, traverse the ifc project object and parse it into something friendly; as well as // Lastly, traverse the ifc project object and parse it into something friendly; as well as
// replace all its geometries with actual references to speckle meshes from the productGeo map // replace all its geometries with actual references to speckle meshes from the productGeo map
await this.traverse( this.project, true, 0 ) await this.traverse(this.project, true, 0)
let id = await this.serverApi.saveObject( this.project ) let id = await this.serverApi.saveObject(this.project)
return { id, tCount: Object.keys( this.project.__closure ).length } return { id, tCount: Object.keys(this.project.__closure).length }
} }
async createGeometries() { async createGeometries() {
this.rawGeo = this.api.LoadAllGeometry( this.modelId ) this.rawGeo = this.api.LoadAllGeometry(this.modelId)
for( let i = 0; i < this.rawGeo.size(); i++ ) { for (let i = 0; i < this.rawGeo.size(); i++) {
const mesh = this.rawGeo.get( i ) const mesh = this.rawGeo.get(i)
const prodId = mesh.expressID const prodId = mesh.expressID
this.productGeo[prodId ] = [] this.productGeo[prodId] = []
for( let j = 0; j < mesh.geometries.size(); j++ ) { for (let j = 0; j < mesh.geometries.size(); j++) {
let placedGeom = mesh.geometries.get( j ) let placedGeom = mesh.geometries.get(j)
let geom = this.api.GetGeometry( this.modelId, placedGeom.geometryExpressID ) let geom = this.api.GetGeometry(this.modelId, placedGeom.geometryExpressID)
let matrix = placedGeom.flatTransformation let matrix = placedGeom.flatTransformation
let raw = { let raw = {
color: geom.color, // NOTE: material: x, y, z = rgb, w = opacity color: geom.color, // NOTE: material: x, y, z = rgb, w = opacity
vertices: this.api.GetVertexArray( geom.GetVertexData(), geom.GetVertexDataSize() ), vertices: this.api.GetVertexArray(
indices: this.api.GetIndexArray( geom.GetIndexData(), geom.GetIndexDataSize() ) geom.GetVertexData(),
geom.GetVertexDataSize()
),
indices: this.api.GetIndexArray(geom.GetIndexData(), geom.GetIndexDataSize())
} }
const { vertices } = this.extractVertexData( raw.vertices ) const { vertices } = this.extractVertexData(raw.vertices)
for( let k = 0; k < vertices.length; k += 3 ) { for (let k = 0; k < vertices.length; k += 3) {
let x = vertices[k], y = vertices[k + 1], z = vertices[k + 2] let x = vertices[k],
y = vertices[k + 1],
z = vertices[k + 2]
vertices[k] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12] vertices[k] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]
vertices[k + 1] = ( matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14] ) * -1 vertices[k + 1] =
(matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]) * -1
vertices[k + 2] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13] vertices[k + 2] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]
} }
// Since all faces are triangles, we must add a `0` before each group of 3. // Since all faces are triangles, we must add a `0` before each group of 3.
let spcklFaces = [ ] let spcklFaces = []
for ( let i = 0; i < raw.indices.length; i++ ) { for (let i = 0; i < raw.indices.length; i++) {
if( i % 3 === 0 ) if (i % 3 === 0) spcklFaces.push(0)
spcklFaces.push( 0 ) spcklFaces.push(raw.indices[i])
spcklFaces.push( raw.indices[i] )
} }
// Create a propper Speckle Mesh // Create a propper Speckle Mesh
@@ -79,40 +86,61 @@ module.exports = class IFCParser {
volume: 0, volume: 0,
area: 0, area: 0,
faces: spcklFaces, faces: spcklFaces,
vertices: Array.from( vertices ), vertices: Array.from(vertices),
renderMaterial: placedGeom.color ? this.colorToMaterial( placedGeom.color ) : null renderMaterial: placedGeom.color
? this.colorToMaterial(placedGeom.color)
: null
} }
let id = await this.serverApi.saveObject( spcklMesh ) let id = await this.serverApi.saveObject(spcklMesh)
let ref = { speckle_type: 'reference', referencedId: id } let ref = { speckle_type: 'reference', referencedId: id }
this.productGeo[prodId].push( ref ) this.productGeo[prodId].push(ref)
} }
} }
} }
async traverse( element, recursive = true, depth = 0, specialTypes = [ { type: 'IfcProject', key: 'Name' }, { type: 'IfcBuilding', key: 'Name' }, { type: 'IfcSite', key: 'Name' } ] ) { async traverse(
element,
recursive = true,
depth = 0,
specialTypes = [
{ type: 'IfcProject', key: 'Name' },
{ type: 'IfcBuilding', key: 'Name' },
{ type: 'IfcSite', key: 'Name' }
]
) {
// Fast exit if null/undefined // Fast exit if null/undefined
if ( !element ) return if (!element) return
// If array, traverse all items in it. // If array, traverse all items in it.
if( Array.isArray( element ) ) { if (Array.isArray(element)) {
return await Promise.all( element.map( async el => await this.traverse( el,recursive, depth + 1 , specialTypes ) ) ) return await Promise.all(
element.map(
async (el) => await this.traverse(el, recursive, depth + 1, specialTypes)
)
)
} }
// If it has no expressID, its either a simple type or a { type, value } object. // If it has no expressID, its either a simple type or a { type, value } object.
if( !element.expressID ) { if (!element.expressID) {
return await Promise.resolve( element.value !== null && element.value !== undefined ? element.value : element ) return await Promise.resolve(
element.value !== null && element.value !== undefined ? element.value : element
)
} }
if( this.cache[element.expressID.toString()] ) return this.cache[element.expressID.toString()] if (this.cache[element.expressID.toString()])
return this.cache[element.expressID.toString()]
// If you got here -> It's an IFC Element: create base object, upload and return ref. // If you got here -> It's an IFC Element: create base object, upload and return ref.
// console.log( `Traversing element ${element.expressID}; Recurse: ${recursive}; Stack ${depth}` ) // console.log( `Traversing element ${element.expressID}; Recurse: ${recursive}; Stack ${depth}` )
// Traverse all key/value pairs first. // Traverse all key/value pairs first.
for( let key of Object.keys( element ) ) { for (let key of Object.keys(element)) {
element[key] = await this.traverse( element[key], recursive, depth + 1, specialTypes ) element[key] = await this.traverse(
element[key],
recursive,
depth + 1,
specialTypes
)
} }
// Assign speckle_type and empty closure table. // Assign speckle_type and empty closure table.
@@ -120,46 +148,126 @@ module.exports = class IFCParser {
element.__closure = {} element.__closure = {}
// Find spatial children and assign to element // Find spatial children and assign to element
const spatialChildrenIds = this.getAllRelatedItemsOfType( element.expressID, WebIFC.IFCRELAGGREGATES, 'RelatingObject', 'RelatedObjects' ) const spatialChildrenIds = this.getAllRelatedItemsOfType(
if( spatialChildrenIds.length > 0 ) element.rawSpatialChildren = spatialChildrenIds.map( ( childId ) => this.api.GetLine( this.modelId, childId, true ) ) element.expressID,
WebIFC.IFCRELAGGREGATES,
'RelatingObject',
'RelatedObjects'
)
if (spatialChildrenIds.length > 0)
element.rawSpatialChildren = spatialChildrenIds.map((childId) =>
this.api.GetLine(this.modelId, childId, true)
)
// Find children and populate element // Find children and populate element
const childrenIds = this.getAllRelatedItemsOfType( element.expressID, WebIFC.IFCRELCONTAINEDINSPATIALSTRUCTURE, 'RelatingStructure', 'RelatedElements' ) const childrenIds = this.getAllRelatedItemsOfType(
if( childrenIds.length > 0 ) element.rawChildren = childrenIds.map( ( childId ) => this.api.GetLine( this.modelId, childId, true ) ) element.expressID,
WebIFC.IFCRELCONTAINEDINSPATIALSTRUCTURE,
'RelatingStructure',
'RelatedElements'
)
if (childrenIds.length > 0)
element.rawChildren = childrenIds.map((childId) =>
this.api.GetLine(this.modelId, childId, true)
)
// Find related property sets // Find related property sets
const psetsIds = this.getAllRelatedItemsOfType( element.expressID, WebIFC.IFCRELDEFINESBYPROPERTIES, 'RelatingPropertyDefinition', 'RelatedObjects' ) const psetsIds = this.getAllRelatedItemsOfType(
if( psetsIds.length > 0 ) element.rawPsets = psetsIds.map( ( childId ) => this.api.GetLine( this.modelId, childId, true ) ) element.expressID,
WebIFC.IFCRELDEFINESBYPROPERTIES,
'RelatingPropertyDefinition',
'RelatedObjects'
)
if (psetsIds.length > 0)
element.rawPsets = psetsIds.map((childId) =>
this.api.GetLine(this.modelId, childId, true)
)
// Find related type properties // Find related type properties
const typePropsId = this.getAllRelatedItemsOfType( element.expressID, WebIFC.IFCRELDEFINESBYTYPE, 'RelatingType', 'RelatedObjects' ) const typePropsId = this.getAllRelatedItemsOfType(
if( typePropsId.length > 0 ) element.rawTypeProps = typePropsId.map( ( childId ) => this.api.GetLine( this.modelId, childId, true ) ) element.expressID,
WebIFC.IFCRELDEFINESBYTYPE,
'RelatingType',
'RelatedObjects'
)
if (typePropsId.length > 0)
element.rawTypeProps = typePropsId.map((childId) =>
this.api.GetLine(this.modelId, childId, true)
)
// Lookup geometry in generated geometries object // Lookup geometry in generated geometries object
if( this.productGeo[element.expressID] ) { if (this.productGeo[element.expressID]) {
element['@displayValue'] = this.productGeo[element.expressID] element['@displayValue'] = this.productGeo[element.expressID]
this.productGeo[element.expressID].forEach( ref => { this.productGeo[element.expressID].forEach((ref) => {
this.project.__closure[ref.referencedId.toString()] = depth this.project.__closure[ref.referencedId.toString()] = depth
element.__closure[ref.referencedId.toString()] = 1 element.__closure[ref.referencedId.toString()] = 1
} ) })
} }
const isSpecial = specialTypes.find( t => t.type === element.speckle_type ) const isSpecial = specialTypes.find((t) => t.type === element.speckle_type)
// Recurse all children // Recurse all children
if ( recursive ) { if (recursive) {
await this.processSubElements( element, 'rawSpatialChildren', 'spatialChildren', isSpecial, recursive, depth, specialTypes ) await this.processSubElements(
await this.processSubElements( element, 'rawChildren', 'children', isSpecial, recursive, depth, specialTypes ) element,
await this.processSubElements( element, 'rawPsets', 'propertySets', false, recursive, depth, specialTypes ) 'rawSpatialChildren',
await this.processSubElements( element, 'rawTypeProps', 'typeProps', false, recursive, depth, specialTypes ) 'spatialChildren',
isSpecial,
recursive,
depth,
specialTypes
)
await this.processSubElements(
element,
'rawChildren',
'children',
isSpecial,
recursive,
depth,
specialTypes
)
await this.processSubElements(
element,
'rawPsets',
'propertySets',
false,
recursive,
depth,
specialTypes
)
await this.processSubElements(
element,
'rawTypeProps',
'typeProps',
false,
recursive,
depth,
specialTypes
)
if( element.children || element.spatialChildren || element.propertySets || element.typeProps ) { if (
console.log( `${element.constructor.name} ${element.GlobalId}:\n\tchildren count: ${ element.children ? element.children.length : '0'};\n\tspatial children count: ${element.spatialChildren ? element.spatialChildren.length : '0'};\n\tproperty sets count: ${element.propertySets ? element.propertySets.length : 0};\n\ttype properties: ${element.typeProps ? element.typeProps.length : 0}` ) element.children ||
element.spatialChildren ||
element.propertySets ||
element.typeProps
) {
console.log(
`${element.constructor.name} ${element.GlobalId}:\n\tchildren count: ${
element.children ? element.children.length : '0'
};\n\tspatial children count: ${
element.spatialChildren ? element.spatialChildren.length : '0'
};\n\tproperty sets count: ${
element.propertySets ? element.propertySets.length : 0
};\n\ttype properties: ${element.typeProps ? element.typeProps.length : 0}`
)
}
} }
} if (
this.productGeo[element.expressID] ||
if( this.productGeo[element.expressID] || element.spatialChildren || element.children ) { element.spatialChildren ||
let id = await this.serverApi.saveObject( element ) element.children
) {
let id = await this.serverApi.saveObject(element)
let ref = { speckle_type: 'reference', referencedId: id } let ref = { speckle_type: 'reference', referencedId: id }
this.cache[element.expressID.toString()] = ref this.cache[element.expressID.toString()] = ref
this.closureCache[element.expressID.toString()] = element.__closure this.closureCache[element.expressID.toString()] = element.__closure
@@ -171,34 +279,38 @@ module.exports = class IFCParser {
} }
} }
async processSubElements(
async processSubElements( element, key, newKey, isSpecial, recursive, depth, specialTypes ) { element,
if ( element[key] ) { key,
if ( !isSpecial ) newKey,
element[newKey] = [] isSpecial,
recursive,
depth,
specialTypes
) {
if (element[key]) {
if (!isSpecial) element[newKey] = []
let childCount = {} let childCount = {}
for ( let child of element[key] ) { for (let child of element[key]) {
let res = await this.traverse( child, recursive, depth + 1, specialTypes ) let res = await this.traverse(child, recursive, depth + 1, specialTypes)
if ( res.referencedId ) { if (res.referencedId) {
if ( isSpecial ) { if (isSpecial) {
let name = child[isSpecial.key] let name = child[isSpecial.key]
if ( !name || name.length === 0 ) if (!name || name.length === 0) name = 'Undefined'
name = 'Undefined' if (!childCount[name]) childCount[name] = 0
if ( !childCount[name] ) if (childCount[name] > 0) name += '-' + childCount[name]++
childCount[name] = 0
if ( childCount[name] > 0 )
name += '-' + childCount[name]++
element[name] = res element[name] = res
} } else element[newKey].push(res)
else
element[newKey].push( res )
this.project.__closure[res.referencedId.toString()] = depth this.project.__closure[res.referencedId.toString()] = depth
element.__closure[res.referencedId.toString()] = 1 element.__closure[res.referencedId.toString()] = 1
// adds to parent (this element) the child's closure tree. // adds to parent (this element) the child's closure tree.
if ( this.closureCache[child.expressID.toString()] ) { if (this.closureCache[child.expressID.toString()]) {
for ( let key of Object.keys( this.closureCache[child.expressID.toString()] ) ) { for (let key of Object.keys(
element.__closure[key] = this.closureCache[child.expressID.toString()][key] + 1 this.closureCache[child.expressID.toString()]
)) {
element.__closure[key] =
this.closureCache[child.expressID.toString()][key] + 1
} }
} }
} }
@@ -208,45 +320,46 @@ module.exports = class IFCParser {
} }
// (c) https://github.com/agviegas/web-ifc-three // (c) https://github.com/agviegas/web-ifc-three
extractVertexData( vertexData ) { extractVertexData(vertexData) {
const vertices = [] const vertices = []
const normals = [] const normals = []
let isNormalData = false let isNormalData = false
for ( let i = 0; i < vertexData.length; i++ ) { for (let i = 0; i < vertexData.length; i++) {
isNormalData ? normals.push( vertexData[i] ) : vertices.push( vertexData[i] ) isNormalData ? normals.push(vertexData[i]) : vertices.push(vertexData[i])
if ( ( i + 1 ) % 3 === 0 ) isNormalData = !isNormalData if ((i + 1) % 3 === 0) isNormalData = !isNormalData
} }
return { vertices, normals } return { vertices, normals }
} }
// (c) https://github.com/agviegas/web-ifc-three/blob/907e08b5673d5e1c18261a4fceade7189d6b2db7/src/IFC/PropertyManager.ts#L110 // (c) https://github.com/agviegas/web-ifc-three/blob/907e08b5673d5e1c18261a4fceade7189d6b2db7/src/IFC/PropertyManager.ts#L110
getAllRelatedItemsOfType( elementID, type, relation, relatedProperty ) { getAllRelatedItemsOfType(elementID, type, relation, relatedProperty) {
const lines = this.api.GetLineIDsWithType( this.modelId, type ) const lines = this.api.GetLineIDsWithType(this.modelId, type)
const IDs = [] const IDs = []
for ( let i = 0; i < lines.size(); i++ ) { for (let i = 0; i < lines.size(); i++) {
const relID = lines.get( i ) const relID = lines.get(i)
const rel = this.api.GetLine( this.modelId, relID ) const rel = this.api.GetLine(this.modelId, relID)
const relatedItems = rel[relation] const relatedItems = rel[relation]
let foundElement = false let foundElement = false
if ( Array.isArray( relatedItems ) ) { if (Array.isArray(relatedItems)) {
const values = relatedItems.map( ( item ) => item.value ) const values = relatedItems.map((item) => item.value)
foundElement = values.includes( elementID ) foundElement = values.includes(elementID)
} else foundElement = ( relatedItems.value === elementID ) } else foundElement = relatedItems.value === elementID
if ( foundElement ) { if (foundElement) {
const element = rel[relatedProperty] const element = rel[relatedProperty]
if ( !Array.isArray( element ) ) IDs.push( element.value ) if (!Array.isArray(element)) IDs.push(element.value)
else element.forEach( ( ele ) => IDs.push( ele.value ) ) else element.forEach((ele) => IDs.push(ele.value))
} }
} }
return IDs return IDs
} }
colorToMaterial( color ) { colorToMaterial(color) {
let intColor = ( color.w << 24 ) + ( ( color.x * 255 ) << 16 ) + ( ( color.y * 255 ) << 8 ) + ( ( color.z * 255 ) ) let intColor =
(color.w << 24) + ((color.x * 255) << 16) + ((color.y * 255) << 8) + color.z * 255
return { return {
diffuse: intColor, diffuse: intColor,
+4 -3
View File
@@ -1,8 +1,9 @@
'use strict' 'use strict'
module.exports = require( 'knex' )( { module.exports = require('knex')({
client: 'pg', client: 'pg',
connection: process.env.PG_CONNECTION_STRING || 'postgres://speckle:speckle@localhost/speckle', connection:
process.env.PG_CONNECTION_STRING || 'postgres://speckle:speckle@localhost/speckle',
pool: { min: 1, max: 1 } pool: { min: 1, max: 1 }
// migrations are in managed in the server package // migrations are in managed in the server package
} ) })
+82 -74
View File
@@ -1,12 +1,12 @@
'use strict' 'use strict'
const knex = require( '../knex' ) const knex = require('../knex')
const { getFileStream } = require( './filesApi' ) const { getFileStream } = require('./filesApi')
const fs = require( 'fs' ) const fs = require('fs')
const { spawn } = require( 'child_process' ) const { spawn } = require('child_process')
const ServerAPI = require( '../ifc/api' ) const ServerAPI = require('../ifc/api')
const HEALTHCHECK_FILE_PATH = '/tmp/last_successful_query' const HEALTHCHECK_FILE_PATH = '/tmp/last_successful_query'
@@ -16,7 +16,7 @@ const TMP_RESULTS_PATH = '/tmp/import_result.json'
let shouldExit = false let shouldExit = false
async function startTask() { async function startTask() {
let { rows } = await knex.raw( ` let { rows } = await knex.raw(`
UPDATE file_uploads UPDATE file_uploads
SET SET
"convertedStatus" = 1, "convertedStatus" = 1,
@@ -29,37 +29,45 @@ async function startTask() {
) as task ) as task
WHERE file_uploads."id" = task."id" WHERE file_uploads."id" = task."id"
RETURNING file_uploads."id" RETURNING file_uploads."id"
` ) `)
return rows[0] return rows[0]
} }
async function doTask( task ) { async function doTask(task) {
let tempUserToken = null let tempUserToken = null
let serverApi = null let serverApi = null
try { try {
console.log( 'Doing task ', task ) console.log('Doing task ', task)
let { rows } = await knex.raw( ` let { rows } = await knex.raw(
`
SELECT SELECT
id as "fileId", "streamId", "branchName", "userId", "fileName", "fileType" id as "fileId", "streamId", "branchName", "userId", "fileName", "fileType"
FROM file_uploads FROM file_uploads
WHERE id = ? WHERE id = ?
LIMIT 1 LIMIT 1
`, [ task.id ] ) `,
[task.id]
)
let info = rows[0] let info = rows[0]
if ( !info ) { if (!info) {
throw new Error( 'Internal error: DB inconsistent' ) throw new Error('Internal error: DB inconsistent')
} }
let upstreamFileStream = await getFileStream( { fileId: info.fileId } ) let upstreamFileStream = await getFileStream({ fileId: info.fileId })
let diskFileStream = fs.createWriteStream( TMP_FILE_PATH ) let diskFileStream = fs.createWriteStream(TMP_FILE_PATH)
upstreamFileStream.pipe( diskFileStream ) upstreamFileStream.pipe(diskFileStream)
await new Promise( fulfill => diskFileStream.on( 'finish' , fulfill ) ) await new Promise((fulfill) => diskFileStream.on('finish', fulfill))
serverApi = new ServerAPI( { streamId: info.streamId } ) serverApi = new ServerAPI({ streamId: info.streamId })
let { token } = await serverApi.createToken( { userId: info.userId, name: 'temp upload token', scopes: [ 'streams:write', 'streams:read' ], lifespan: 1000000 } ) let { token } = await serverApi.createToken({
userId: info.userId,
name: 'temp upload token',
scopes: ['streams:write', 'streams:read'],
lifespan: 1000000
})
tempUserToken = token tempUserToken = token
await runProcessWithTimeout( await runProcessWithTimeout(
@@ -78,14 +86,14 @@ async function doTask( task ) {
10 * 60 * 1000 10 * 60 * 1000
) )
let output = JSON.parse( fs.readFileSync( TMP_RESULTS_PATH ) ) let output = JSON.parse(fs.readFileSync(TMP_RESULTS_PATH))
if ( !output.success ) if (!output.success) throw new Error(output.error)
throw new Error( output.error )
let commitId = output.commitId let commitId = output.commitId
await knex.raw( ` await knex.raw(
`
UPDATE file_uploads UPDATE file_uploads
SET SET
"convertedStatus" = 2, "convertedStatus" = 2,
@@ -93,103 +101,103 @@ async function doTask( task ) {
"convertedMessage" = 'File converted successfully', "convertedMessage" = 'File converted successfully',
"convertedCommitId" = ? "convertedCommitId" = ?
WHERE "id" = ? WHERE "id" = ?
`, [ commitId, task.id ] ) `,
} catch ( err ) { [commitId, task.id]
console.log( 'Error: ', err ) )
await knex.raw( ` } catch (err) {
console.log('Error: ', err)
await knex.raw(
`
UPDATE file_uploads UPDATE file_uploads
SET SET
"convertedStatus" = 3, "convertedStatus" = 3,
"convertedLastUpdate" = NOW(), "convertedLastUpdate" = NOW(),
"convertedMessage" = ? "convertedMessage" = ?
WHERE "id" = ? WHERE "id" = ?
`, [ err.toString(), task.id ] ) `,
[err.toString(), task.id]
)
} }
if ( fs.existsSync( TMP_FILE_PATH ) ) fs.unlinkSync( TMP_FILE_PATH ) if (fs.existsSync(TMP_FILE_PATH)) fs.unlinkSync(TMP_FILE_PATH)
if ( fs.existsSync( TMP_RESULTS_PATH ) ) fs.unlinkSync( TMP_RESULTS_PATH ) if (fs.existsSync(TMP_RESULTS_PATH)) fs.unlinkSync(TMP_RESULTS_PATH)
if ( tempUserToken ) { if (tempUserToken) {
await serverApi.revokeTokenById( tempUserToken ) await serverApi.revokeTokenById(tempUserToken)
} }
} }
function runProcessWithTimeout( cmd, cmdArgs, extraEnv, timeoutMs ) { function runProcessWithTimeout(cmd, cmdArgs, extraEnv, timeoutMs) {
return new Promise((resolve, reject) => {
console.log(`Starting process: ${cmd} ${cmdArgs}`)
const childProc = spawn(cmd, cmdArgs, { env: { ...process.env, ...extraEnv } })
return new Promise( ( resolve, reject ) => { childProc.stdout.on('data', (data) => {
console.log( `Starting process: ${cmd} ${cmdArgs}` ) console.log('Parser: ', data.toString())
const childProc = spawn( cmd, cmdArgs, { env: { ...process.env, ...extraEnv } } ) })
childProc.stdout.on( 'data', ( data ) => { childProc.stderr.on('data', (data) => {
console.log( 'Parser: ', data.toString() ) console.error('Parser: ', data.toString())
} ) })
childProc.stderr.on( 'data', ( data ) => {
console.error( 'Parser: ', data.toString() )
} )
let timedOut = false let timedOut = false
let timeout = setTimeout( () => { let timeout = setTimeout(() => {
console.log( 'Process timeout. Killing process...' ) console.log('Process timeout. Killing process...')
timedOut = true timedOut = true
childProc.kill( 9 ) childProc.kill(9)
reject( `Timeout: Process took longer than ${timeoutMs} ms to execute` ) reject(`Timeout: Process took longer than ${timeoutMs} ms to execute`)
}, timeoutMs ) }, timeoutMs)
childProc.on( 'close', ( code ) => { childProc.on('close', (code) => {
console.log( `Process exited with code ${code}` ) console.log(`Process exited with code ${code}`)
if ( timedOut ) return // ignore `close` calls after killing (the promise was already rejected) if (timedOut) return // ignore `close` calls after killing (the promise was already rejected)
clearTimeout( timeout ) clearTimeout(timeout)
if ( code === 0 ) { if (code === 0) {
resolve() resolve()
} else { } else {
reject( `Parser exited with code ${code}` ) reject(`Parser exited with code ${code}`)
} }
} ) })
})
} )
} }
async function tick() { async function tick() {
if ( shouldExit ) { if (shouldExit) {
process.exit( 0 ) process.exit(0)
} }
try { try {
let task = await startTask() let task = await startTask()
fs.writeFile( HEALTHCHECK_FILE_PATH, '' + Date.now(), () => {} ) fs.writeFile(HEALTHCHECK_FILE_PATH, '' + Date.now(), () => {})
if ( !task ) { if (!task) {
setTimeout( tick, 1000 ) setTimeout(tick, 1000)
return return
} }
await doTask( task ) await doTask(task)
// Check for another task very soon // Check for another task very soon
setTimeout( tick, 10 ) setTimeout(tick, 10)
} catch ( err ) { } catch (err) {
console.log( 'Error executing task: ', err ) console.log('Error executing task: ', err)
setTimeout( tick, 5000 ) setTimeout(tick, 5000)
} }
} }
async function main() { async function main() {
console.log( 'Starting FileUploads Service...' ) console.log('Starting FileUploads Service...')
process.on( 'SIGTERM', () => { process.on('SIGTERM', () => {
shouldExit = true shouldExit = true
console.log( 'Shutting down...' ) console.log('Shutting down...')
} ) })
tick() tick()
} }
+9
View File
@@ -18,6 +18,15 @@ const config = {
parserOptions: { parserOptions: {
sourceType: 'module' sourceType: 'module'
}, },
overrides: [
{
files: './*.{js, ts}',
env: {
node: true,
commonjs: true
}
}
],
plugins: ['vue'], plugins: ['vue'],
rules: { rules: {
'no-console': 1 'no-console': 1
+1
View File
@@ -37,6 +37,7 @@ npm run serve
``` ```
### Packaging for production ### Packaging for production
If you plan to package the frontend to use in a production setting, see our [Server deployment instructions](https://speckle.guide/dev/server-setup.html) (chapter `Run your speckle-server fork`) If you plan to package the frontend to use in a production setting, see our [Server deployment instructions](https://speckle.guide/dev/server-setup.html) (chapter `Run your speckle-server fork`)
## Community ## Community
+4 -1
View File
@@ -2,7 +2,10 @@ const path = require('path')
// Load .env files // Load .env files
const { loadEnv } = require('vue-cli-plugin-apollo/utils/load-env') const { loadEnv } = require('vue-cli-plugin-apollo/utils/load-env')
const env = loadEnv([path.resolve(__dirname, '.env'), path.resolve(__dirname, '.env.local')]) const env = loadEnv([
path.resolve(__dirname, '.env'),
path.resolve(__dirname, '.env.local')
])
module.exports = { module.exports = {
client: { client: {
+1 -3
View File
@@ -1,6 +1,4 @@
module.exports = { module.exports = {
presets: ['@vue/cli-plugin-babel/preset'], presets: ['@vue/cli-plugin-babel/preset'],
exclude: [ exclude: [/(Speckle.js\.). /]
/(Speckle.js\.). /
]
} }
+57 -31
View File
@@ -2,29 +2,38 @@
<html lang="en"> <html lang="en">
<head> <head>
<!-- <base href="/appname/"> --> <!-- <base href="/appname/"> -->
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0, user-scalable=0"> <meta
<meta property="og:title" content="Speckle"> name="viewport"
<meta property="og:description" content=""> content="width=device-width,initial-scale=1.0,maximum-scale=1.0, user-scalable=0"
<meta property="og:image" content="https://speckle.xyz/og_image.png"> />
<meta property="og:url" content="<%= BASE_URL %>"> <meta property="og:title" content="Speckle" />
<meta name="twitter:card" content="summary_large_image"> <meta property="og:description" content="" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <meta property="og:image" content="https://speckle.xyz/og_image.png" />
<meta property="og:url" content="<%= BASE_URL %>" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500&display=swap" rel="stylesheet"> <link
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"> href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"
/>
<style type="text/css"> <style type="text/css">
body { body {
background-color: #333333; background-color: #333333;
color: #0A66FF; color: #0a66ff;
} }
@media screen and (prefers-color-scheme: light) { @media screen and (prefers-color-scheme: light) {
body { body {
background-color: white !important; background-color: white !important;
color: #0A66FF; color: #0a66ff;
} }
} }
@@ -38,7 +47,7 @@
animation-iteration-count: infinite; animation-iteration-count: infinite;
} }
.hover-tada:hover{ .hover-tada:hover {
-webkit-animation-name: tada; -webkit-animation-name: tada;
animation-name: tada; animation-name: tada;
-webkit-animation-duration: 1s; -webkit-animation-duration: 1s;
@@ -53,15 +62,21 @@
-webkit-transform: scale3d(1, 1, 1); -webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
} }
10%, 20% { 10%,
-webkit-transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg); 20% {
transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg); -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% { 30%,
50%,
70%,
90% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg); -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); transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
} }
40%, 60%, 80% { 40%,
60%,
80% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg); -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); transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
} }
@@ -75,15 +90,21 @@
-webkit-transform: scale3d(1, 1, 1); -webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
} }
10%, 20% { 10%,
-webkit-transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg); 20% {
transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg); -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% { 30%,
50%,
70%,
90% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg); -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); transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
} }
40%, 60%, 80% { 40%,
60%,
80% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg); -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); transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
} }
@@ -96,15 +117,19 @@
</head> </head>
<body> <body>
<noscript> <noscript>
<strong>We're sorry but Speckle doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>
We're sorry but Speckle doesn't work properly without JavaScript enabled. Please
enable it to continue.
</strong>
</noscript> </noscript>
<div id="app"> <div id="app">
<div style=' <div
style="
width: 100%; width: 100%;
height: 300px; height: 300px;
font-family: sans-serif !important; font-family: sans-serif !important;
position: absolute; position: absolute;
top:0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
@@ -112,9 +137,10 @@
text-align: center; text-align: center;
font-weight: 400; font-weight: 400;
font-size: 10px; font-size: 10px;
' "
> >
<img src="<%= BASE_URL %>logo.svg" style="max-width: 50px" class="tada"> <img src="<%= BASE_URL %>logo.svg" style="max-width: 50px" class="tada" />
</div>
</div> </div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
+34 -26
View File
@@ -1,27 +1,30 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head>
<!-- <base href="/appname/"> --> <!-- <base href="/appname/"> -->
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title> <title><%= htmlWebpackPlugin.options.title %></title>
<%= htmlWebpackPlugin.options.title %> <link
</title> rel="stylesheet"
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"> 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"> />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"
/>
<style type="text/css"> <style type="text/css">
body { body {
background-color: #333333; background-color: #333333;
color: #0A66FF; color: #0a66ff;
} }
@media screen and (prefers-color-scheme: light) { @media screen and (prefers-color-scheme: light) {
body { body {
background-color: white; background-color: white;
color: #0A66FF; color: #0a66ff;
} }
} }
@@ -43,8 +46,8 @@
10%, 10%,
20% { 20% {
-webkit-transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg); -webkit-transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg); transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
} }
30%, 30%,
@@ -76,8 +79,8 @@
10%, 10%,
20% { 20% {
-webkit-transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg); -webkit-transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg); transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
} }
30%, 30%,
@@ -101,19 +104,23 @@
} }
} }
</style> </style>
</head> </head>
<body> <body>
<noscript> <noscript>
<strong>We're sorry but Speckle doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>
We're sorry but Speckle doesn't work properly without JavaScript enabled. Please
enable it to continue.
</strong>
</noscript> </noscript>
<div id="app"> <div id="app">
<div style=' <div
style="
width: 100%; width: 100%;
height: 300px; height: 300px;
font-family: sans-serif !important; font-family: sans-serif !important;
position: absolute; position: absolute;
top:0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
@@ -121,10 +128,11 @@
text-align: center; text-align: center;
font-weight: 400; font-weight: 400;
font-size: 10px; font-size: 10px;
'> "
<img src="<%= BASE_URL %>logo.svg" style="max-width: 50px" class="tada"> >
<img src="<%= BASE_URL %>logo.svg" style="max-width: 50px" class="tada" />
</div>
</div> </div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
</html> </html>
+4 -1
View File
@@ -6,7 +6,10 @@ export default {
components: {}, components: {},
mounted() { mounted() {
let mixpanelId = this.$mixpanelId() let mixpanelId = this.$mixpanelId()
this.$mixpanel.register({ server_id: this.$mixpanelServerId(), hostApp: 'web-embed' }) this.$mixpanel.register({
server_id: this.$mixpanelServerId(),
hostApp: 'web-embed'
})
if (mixpanelId !== null) { if (mixpanelId !== null) {
this.$mixpanel.identify(mixpanelId) this.$mixpanel.identify(mixpanelId)
} }
+17 -6
View File
@@ -1,12 +1,18 @@
<template> <template>
<v-app :class="`no-scrollbar ${$vuetify.theme.dark ? 'background-dark' : 'background-light'}`"> <v-app
:class="`no-scrollbar ${
$vuetify.theme.dark ? 'background-dark' : 'background-light'
}`"
>
<!-- <speckle-loading v-if="!stream || error" :error="error" style="z-index: 101000" /> --> <!-- <speckle-loading v-if="!stream || error" :error="error" style="z-index: 101000" /> -->
<div v-if="!error" style="z-index: 1000"> <div v-if="!error" style="z-index: 1000">
<div class="top-left bottom-left ma-2 d-flex"> <div class="top-left bottom-left ma-2 d-flex">
<span class="caption d-inline-flex align-center"> <span class="caption d-inline-flex align-center">
<img src="@/assets/logo.svg" height="20" /> <img src="@/assets/logo.svg" height="20" />
<span style="margin-top: 2px" class="primary--text"> <span style="margin-top: 2px" class="primary--text">
<a href="https://speckle.xyz" target="_blank" class="text-decoration-none">Speckle</a> <a href="https://speckle.xyz" target="_blank" class="text-decoration-none">
Speckle
</a>
</span> </span>
</span> </span>
</div> </div>
@@ -90,7 +96,9 @@ export default {
}, },
computed: { computed: {
isSmall() { isSmall() {
return this.$vuetify.breakpoint.name == 'xs' || this.$vuetify.breakpoint.name == 'sm' return (
this.$vuetify.breakpoint.name == 'xs' || this.$vuetify.breakpoint.name == 'sm'
)
}, },
displayType() { displayType() {
if (!this.input.stream) { if (!this.input.stream) {
@@ -147,7 +155,8 @@ export default {
let res = await getCommit(this.input.stream, this.input.commit) let res = await getCommit(this.input.stream, this.input.commit)
let data = res.data let data = res.data
let latestCommit = data.stream.commit let latestCommit = data.stream.commit
if (this.input.object === undefined) this.objectId = latestCommit.referencedObject if (this.input.object === undefined)
this.objectId = latestCommit.referencedObject
this.specificCommit = data.stream this.specificCommit = data.stream
} catch (e) { } catch (e) {
this.error = e.message this.error = e.message
@@ -157,13 +166,15 @@ export default {
try { try {
let res = await getLatestBranchCommit(this.input.stream, this.input.branch) let res = await getLatestBranchCommit(this.input.stream, this.input.branch)
let data = res.data let data = res.data
let latestCommit = data.stream.branch.commits.items[0] || data.stream.branch.commit let latestCommit =
data.stream.branch.commits.items[0] || data.stream.branch.commit
if (!latestCommit) { if (!latestCommit) {
this.error = 'No commit for this branch' this.error = 'No commit for this branch'
this.lastCommit = data.stream this.lastCommit = data.stream
return return
} }
if (this.input.object == undefined) this.objectId = latestCommit.referencedObject if (this.input.object == undefined)
this.objectId = latestCommit.referencedObject
else this.objectId = this.input.object else this.objectId = this.input.object
this.lastCommit = data.stream this.lastCommit = data.stream
} catch (e) { } catch (e) {
+5 -1
View File
@@ -1,4 +1,8 @@
import { branchLastCommitQuery, serverInfoQuery, streamCommitQuery } from './speckleQueries.js' import {
branchLastCommitQuery,
serverInfoQuery,
streamCommitQuery
} from './speckleQueries.js'
export let SERVER_URL = window.location.origin export let SERVER_URL = window.location.origin
@@ -1,4 +1,4 @@
query Object($streamId: String!, $id: String!){ query Object($streamId: String!, $id: String!) {
stream(id: $streamId) { stream(id: $streamId) {
id id
object(id: $id) { object(id: $id) {
@@ -1,4 +1,4 @@
query Object($streamId: String!, $id: String!){ query Object($streamId: String!, $id: String!) {
stream(id: $streamId) { stream(id: $streamId) {
id id
name name
@@ -4,7 +4,7 @@ query StreamCommits($id: String!) {
role role
commits { commits {
totalCount totalCount
items{ items {
id id
authorId authorId
authorName authorName
+4 -1
View File
@@ -5,7 +5,10 @@ Vue.prototype.$eventHub = new Vue()
import App from '@/main/App.vue' import App from '@/main/App.vue'
import { createProvider } from '@/vue-apollo' import { createProvider } from '@/vue-apollo'
import { checkAccessCodeAndGetTokens, prefetchUserAndSetSuuid } from '@/plugins/authHelpers' import {
checkAccessCodeAndGetTokens,
prefetchUserAndSetSuuid
} from '@/plugins/authHelpers'
import router from '@/main/router/index' import router from '@/main/router/index'
import vuetify from '@/plugins/vuetify' import vuetify from '@/plugins/vuetify'
@@ -36,11 +36,16 @@
class="mr-3" class="mr-3"
:user-id="activityItem.info.targetUser" :user-id="activityItem.info.targetUser"
:color=" :color="
lastActivity.actionType === 'stream_permissions_add' ? 'success' : 'error' lastActivity.actionType === 'stream_permissions_add'
? 'success'
: 'error'
" "
></user-pill> ></user-pill>
<span v-if="$vuetify.breakpoint.smAndUp" class="mr-3 body-2 font-italic"> <span
v-if="$vuetify.breakpoint.smAndUp"
class="mr-3 body-2 font-italic"
>
{{ {{
lastActivity.actionType === 'stream_permissions_add' lastActivity.actionType === 'stream_permissions_add'
? 'user added as' ? 'user added as'
@@ -85,7 +90,9 @@
<v-icon color="primary" small>mdi-folder</v-icon> <v-icon color="primary" small>mdi-folder</v-icon>
{{ stream.name }} {{ stream.name }}
</router-link> </router-link>
<span class="ml-3 body-2 font-italic">{{ lastActivityBrief.actionText }}</span> <span class="ml-3 body-2 font-italic">
{{ lastActivityBrief.actionText }}
</span>
<v-spacer /> <v-spacer />
@@ -168,10 +175,14 @@
> >
<v-card-text class="pa-5 body-1"> <v-card-text class="pa-5 body-1">
<v-chip :to="url" :color="lastActivityBrief.color"> <v-chip :to="url" :color="lastActivityBrief.color">
<v-icon small class="mr-2 float-left" light>{{ lastActivityBrief.icon }}</v-icon> <v-icon small class="mr-2 float-left" light>
{{ lastActivityBrief.icon }}
</v-icon>
{{ branchName }} {{ branchName }}
</v-chip> </v-chip>
<span class="ml-3 body-2 font-italic">{{ lastActivityBrief.actionText }}</span> <span class="ml-3 body-2 font-italic">
{{ lastActivityBrief.actionText }}
</span>
<div class="mt-3"> <div class="mt-3">
<div <div
v-for="activityItem in activityGroup" v-for="activityItem in activityGroup"
@@ -215,7 +226,9 @@
</v-chip> </v-chip>
<span v-if="lastActivity.actionType === 'commit_create'"> <span v-if="lastActivity.actionType === 'commit_create'">
<span class="mx-3 body-2 font-italic">from</span> <span class="mx-3 body-2 font-italic">from</span>
<source-app-avatar :application-name="commit.sourceApplication" /> <source-app-avatar
:application-name="commit.sourceApplication"
/>
</span> </span>
<span v-if="lastActivity.actionType === 'commit_receive'"> <span v-if="lastActivity.actionType === 'commit_receive'">
<span class="mx-3 body-2 font-italic">in</span> <span class="mx-3 body-2 font-italic">in</span>
@@ -438,13 +451,17 @@ export default {
case 'stream_permissions_add': case 'stream_permissions_add':
return { return {
captionText: `added ${ captionText: `added ${
this.activityGroup.length === 1 ? 'a user' : this.activityGroup.length + ' users' this.activityGroup.length === 1
? 'a user'
: this.activityGroup.length + ' users'
} to` } to`
} }
case 'stream_permissions_remove': case 'stream_permissions_remove':
return { return {
captionText: `removed ${ captionText: `removed ${
this.activityGroup.length === 1 ? 'a user' : this.activityGroup.length + ' users' this.activityGroup.length === 1
? 'a user'
: this.activityGroup.length + ' users'
} from` } from`
} }
case 'branch_create': case 'branch_create':
@@ -7,7 +7,12 @@
<v-icon x-small color="primary" class="mr-1">{{ icons[value.name] }}</v-icon> <v-icon x-small color="primary" class="mr-1">{{ icons[value.name] }}</v-icon>
{{ capitalize(value.name.split('History')[0]) }} history {{ capitalize(value.name.split('History')[0]) }} history
</p> </p>
<apex-chart class="primary--text" type="bar" :options="options" :series="[value]" /> <apex-chart
class="primary--text"
type="bar"
:options="options"
:series="[value]"
/>
</v-col> </v-col>
</v-row> </v-row>
</section-card> </section-card>
@@ -16,13 +16,17 @@
Updated Updated
<b><timeago :datetime="stream.updatedAt"></timeago></b> <b><timeago :datetime="stream.updatedAt"></timeago></b>
<br /> <br />
<span class="grey--text">({{ new Date(stream.updatedAt).toLocaleString() }})</span> <span class="grey--text">
({{ new Date(stream.updatedAt).toLocaleString() }})
</span>
</v-col> </v-col>
<v-col cols="2" class="caption text-truncate"> <v-col cols="2" class="caption text-truncate">
Created Created
<b><timeago :datetime="stream.createdAt"></timeago></b> <b><timeago :datetime="stream.createdAt"></timeago></b>
<br /> <br />
<span class="grey--text">({{ new Date(stream.createdAt).toLocaleString() }})</span> <span class="grey--text">
({{ new Date(stream.createdAt).toLocaleString() }})
</span>
</v-col> </v-col>
<v-col v-tooltip="'Stream total size'" class="caption font-weight-bold"> <v-col v-tooltip="'Stream total size'" class="caption font-weight-bold">
{{ `${(stream.size ? stream.size / 1048576 : 0.0).toFixed(2)} MB` }} {{ `${(stream.size ? stream.size / 1048576 : 0.0).toFixed(2)} MB` }}
@@ -37,7 +41,13 @@
<collaborators-display :stream="stream" :link-to-collabs="false" /> <collaborators-display :stream="stream" :link-to-collabs="false" />
</v-col> </v-col>
<v-col cols="1" class="text-right"> <v-col cols="1" class="text-right">
<v-btn v-tooltip="'Delete stream'" small icon color="error" @click="$emit('delete', stream)"> <v-btn
v-tooltip="'Delete stream'"
small
icon
color="error"
@click="$emit('delete', stream)"
>
<v-icon small>mdi-delete-outline</v-icon> <v-icon small>mdi-delete-outline</v-icon>
</v-btn> </v-btn>
</v-col> </v-col>
@@ -3,12 +3,20 @@
<v-col class="text-truncate"> <v-col class="text-truncate">
<user-avatar :id="selfUser.id" :size="30" class="mr-2"></user-avatar> <user-avatar :id="selfUser.id" :size="30" class="mr-2"></user-avatar>
<router-link class="text-decoration-none space-grotesk mx-1" :to="`/profile/${selfUser.id}`"> <router-link
class="text-decoration-none space-grotesk mx-1"
:to="`/profile/${selfUser.id}`"
>
{{ selfUser.name }} {{ selfUser.name }}
</router-link> </router-link>
</v-col> </v-col>
<v-col cols="3" class="caption text-truncate"> <v-col cols="3" class="caption text-truncate">
<v-icon v-if="selfUser.verified" v-tooltip="'Verfied email'" small class="mr-2 primary--text"> <v-icon
v-if="selfUser.verified"
v-tooltip="'Verfied email'"
small
class="mr-2 primary--text"
>
mdi-shield-check mdi-shield-check
</v-icon> </v-icon>
<v-icon v-else v-tooltip="'Email not verified'" small class="mr-2 warning--text"> <v-icon v-else v-tooltip="'Email not verified'" small class="mr-2 warning--text">
@@ -45,7 +53,13 @@
></v-select> ></v-select>
<!-- </v-col> <!-- </v-col>
<v-col cols="1" class="text-right"> --> <v-col cols="1" class="text-right"> -->
<v-btn v-tooltip="'Delete user'" small icon color="error" @click="$emit('delete', selfUser)"> <v-btn
v-tooltip="'Delete user'"
small
icon
color="error"
@click="$emit('delete', selfUser)"
>
<v-icon small>mdi-delete-outline</v-icon> <v-icon small>mdi-delete-outline</v-icon>
</v-btn> </v-btn>
</v-col> </v-col>
@@ -75,7 +75,9 @@ export default {
}, },
methods: { methods: {
getLatestVersion() { getLatestVersion() {
return fetch('https://api.github.com/repos/specklesystems/speckle-server/releases/latest') return fetch(
'https://api.github.com/repos/specklesystems/speckle-server/releases/latest'
)
.then(async (res) => { .then(async (res) => {
let x = await res.json() let x = await res.json()
return x.tag_name return x.tag_name
@@ -3,18 +3,26 @@
<v-card-text class="text-h3 text-sm-h4 text-md-h3 primary--text"> <v-card-text class="text-h3 text-sm-h4 text-md-h3 primary--text">
<span class="primary--text"> <span class="primary--text">
<b> <b>
<a class="text-decoration-none" href="https://speckle.systems" target="_blank">Speckle</a> <a
class="text-decoration-none"
href="https://speckle.systems"
target="_blank"
>
Speckle
</a>
</b> </b>
</span> </span>
<span class="font-weight-light">, empowering your design and construction data.</span> <span class="font-weight-light">
, empowering your design and construction data.
</span>
</v-card-text> </v-card-text>
<div class=""> <div class="">
<v-card-text class="text-h6 font-weight-regular"> <v-card-text class="text-h6 font-weight-regular">
Speckle helps leading AEC companies freely exchange data between software silos and automate Speckle helps leading AEC companies freely exchange data between software silos
design and delivery processes: and automate design and delivery processes:
<span class="primary--text text--disabled"> <span class="primary--text text--disabled">
join 100s of designers, architects, engineers and developers building the digital future join 100s of designers, architects, engineers and developers building the
of AEC. digital future of AEC.
</span> </span>
</v-card-text> </v-card-text>
</div> </div>
@@ -7,7 +7,12 @@
</v-card-title> </v-card-title>
<v-card-text class="pb-5"> <v-card-text class="pb-5">
<template v-for="s in strategies"> <template v-for="s in strategies">
<v-col :key="s.name" cols="12" class="text-center py-1 my-0" @click="trackSignIn(s.name)"> <v-col
:key="s.name"
cols="12"
class="text-center py-1 my-0"
@click="trackSignIn(s.name)"
>
<v-btn <v-btn
dark dark
block block
@@ -1,6 +1,10 @@
<template> <template>
<div v-if="user" style="display: inline-block" class="text-center"> <div v-if="user" style="display: inline-block" class="text-center">
<user-avatar-icon :size="size" :avatar="user.avatar" :seed="user.id"></user-avatar-icon> <user-avatar-icon
:size="size"
:avatar="user.avatar"
:seed="user.id"
></user-avatar-icon>
<p class="text-h6 mt-4"> <p class="text-h6 mt-4">
{{ user.name }} {{ user.name }}
<br /> <br />
@@ -23,14 +23,24 @@
<!-- <br /> --> <!-- <br /> -->
<span v-if="commentDetails.replies.totalCount > 0"> <span v-if="commentDetails.replies.totalCount > 0">
<!-- eslint-disable-next-line prettier/prettier --> <!-- eslint-disable-next-line prettier/prettier -->
Last reply <timeago :datetime="commentDetails.updatedAt" /> <!--, on {{ new Date(commentDetails.updatedAt).toLocaleString() }} --> Last reply
<timeago :datetime="commentDetails.updatedAt" />
<!--, on {{ new Date(commentDetails.updatedAt).toLocaleString() }} -->
<br /> <br />
</span> </span>
<span class="grey--text"> <span class="grey--text">
Created on {{ new Date(commentDetails.createdAt).toLocaleString() }} Created on {{ new Date(commentDetails.createdAt).toLocaleString() }}
</span> </span>
<br> <br />
<v-btn v-if="canArchiveThread" @click="showArchiveDialog=true" class="ml-n2 red--text rounded-lg elevation-0" x-small plain>Archive</v-btn> <v-btn
v-if="canArchiveThread"
@click="showArchiveDialog = true"
class="ml-n2 red--text rounded-lg elevation-0"
x-small
plain
>
Archive
</v-btn>
<v-dialog v-model="showArchiveDialog" max-width="500"> <v-dialog v-model="showArchiveDialog" max-width="500">
<v-card> <v-card>
<v-toolbar color="error" dark flat> <v-toolbar color="error" dark flat>
@@ -39,7 +49,9 @@
</v-app-bar-nav-icon> </v-app-bar-nav-icon>
<v-toolbar-title>Archive Comment Thread</v-toolbar-title> <v-toolbar-title>Archive Comment Thread</v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon @click="showArchiveDialog = false"><v-icon>mdi-close</v-icon></v-btn> <v-btn icon @click="showArchiveDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar> </v-toolbar>
<v-card-text class="mt-4"> <v-card-text class="mt-4">
This comment thread will be archived. Are you sure? This comment thread will be archived. Are you sure?
@@ -51,7 +63,15 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-btn v-if="isUnread" @click="markAsRead" class="ml-n2 rounded-lg elevation-0" x-small plain>Mark as read</v-btn> <v-btn
v-if="isUnread"
@click="markAsRead"
class="ml-n2 rounded-lg elevation-0"
x-small
plain
>
Mark as read
</v-btn>
</div> </div>
</div> </div>
<div class="body-2 px-4 flex-shrink-0"> <div class="body-2 px-4 flex-shrink-0">
@@ -103,12 +123,17 @@ export default {
}, },
props: { props: {
comment: { type: Object, default: () => null }, comment: { type: Object, default: () => null },
stream: { type: Object, default: () => { return { role: null } } } stream: {
type: Object,
default: () => {
return { role: null }
}
}
}, },
apollo: { apollo: {
commentDetails: { commentDetails: {
query: gql` query: gql`
query($streamId: String!, $id: String!) { query ($streamId: String!, $id: String!) {
comment(streamId: $streamId, id: $id) { comment(streamId: $streamId, id: $id) {
id id
text text
@@ -146,7 +171,7 @@ export default {
$subscribe: { $subscribe: {
commentThreadActivity: { commentThreadActivity: {
query: gql` query: gql`
subscription($streamId: String!, $commentId: String!) { subscription ($streamId: String!, $commentId: String!) {
commentThreadActivity(streamId: $streamId, commentId: $commentId) commentThreadActivity(streamId: $streamId, commentId: $commentId)
} }
`, `,
@@ -160,7 +185,7 @@ export default {
return !this.$loggedIn() return !this.$loggedIn()
}, },
result({ data }) { result({ data }) {
if(!data || !data.commentThreadActivity) return if (!data || !data.commentThreadActivity) return
if (data.commentThreadActivity.eventType === 'reply-added') { if (data.commentThreadActivity.eventType === 'reply-added') {
this.commentDetails.replies.totalCount++ this.commentDetails.replies.totalCount++
this.commentDetails.updatedAt = Date.now() this.commentDetails.updatedAt = Date.now()
@@ -181,9 +206,13 @@ export default {
}, },
computed: { computed: {
canArchiveThread() { canArchiveThread() {
if(!this.comment || !this.stream ) return false if (!this.comment || !this.stream) return false
if(!this.stream.role) return false if (!this.stream.role) return false
if(this.comment.authorId === this.$userId() || this.stream.role ==='stream:owner') return true if (
this.comment.authorId === this.$userId() ||
this.stream.role === 'stream:owner'
)
return true
}, },
link() { link() {
if (!this.commentDetails) return if (!this.commentDetails) return
@@ -197,10 +226,14 @@ export default {
}, },
isUnread() { isUnread() {
if (!this.commentDetails) return if (!this.commentDetails) return
return new Date(this.commentDetails.updatedAt) - new Date(this.commentDetails.viewedAt) > 0 return (
new Date(this.commentDetails.updatedAt) -
new Date(this.commentDetails.viewedAt) >
0
)
} }
}, },
methods:{ methods: {
async markAsRead() { async markAsRead() {
this.commentDetails.viewedAt = Date.now() this.commentDetails.viewedAt = Date.now()
await this.$apollo.mutate({ await this.$apollo.mutate({
@@ -209,7 +242,10 @@ export default {
commentView(streamId: $streamId, commentId: $commentId) commentView(streamId: $streamId, commentId: $commentId)
} }
`, `,
variables: { streamId: this.$route.params.streamId, commentId: this.comment.id } variables: {
streamId: this.$route.params.streamId,
commentId: this.comment.id
}
}) })
}, },
async archiveComment() { async archiveComment() {
@@ -1,10 +1,17 @@
<template> <template>
<div <div
class="no-mouse pa-2" class="no-mouse pa-2"
:style="`${$vuetify.breakpoint.xs ? 'width: 90vw;' : 'width: 300px;'} xxx-background: rgba(0.5, 0.5, 0.5, 0.5)`" :style="`${
$vuetify.breakpoint.xs ? 'width: 90vw;' : 'width: 300px;'
} xxx-background: rgba(0.5, 0.5, 0.5, 0.5)`"
> >
<div v-if="$vuetify.breakpoint.xs" class="text-right mb-5 mouse"> <div v-if="$vuetify.breakpoint.xs" class="text-right mb-5 mouse">
<v-btn icon small class="background ml-2 elevation-10" @click="minimise = !minimise"> <v-btn
icon
small
class="background ml-2 elevation-10"
@click="minimise = !minimise"
>
<v-icon v-if="!minimise" small>mdi-minus</v-icon> <v-icon v-if="!minimise" small>mdi-minus</v-icon>
<v-icon v-else small>mdi-plus</v-icon> <v-icon v-else small>mdi-plus</v-icon>
</v-btn> </v-btn>
@@ -18,17 +25,27 @@
</v-btn> </v-btn>
</div> </div>
<div v-show="!minimise" style="width: 100%" class="mouse"> <div v-show="!minimise" style="width: 100%" class="mouse">
<div v-if="!isComplete" class="warning rounded-xl py-2 caption mb-2 text-center" dense> <div
v-if="!isComplete"
class="warning rounded-xl py-2 caption mb-2 text-center"
dense
>
<v-icon x-small>mdi-alert-circle-outline</v-icon> <v-icon x-small>mdi-alert-circle-outline</v-icon>
This comment is targeting other resources. This comment is targeting other resources.
<v-btn x-small @click="addMissingResources()">View in full context</v-btn> <v-btn x-small @click="addMissingResources()">View in full context</v-btn>
</div> </div>
<div class="px-2" v-show="$apollo.loading"> <div class="px-2" v-show="$apollo.loading">
<v-progress-linear indeterminate/> <v-progress-linear indeterminate />
</div> </div>
<template v-for="(reply, index) in thread"> <template v-for="(reply, index) in thread">
<div v-if="showTime(index)" :key="index + 'date'" class="d-flex justify-center mouse"> <div
<div class="d-inline px-2 py-0 caption text-center mb-2 rounded-lg background grey--text"> v-if="showTime(index)"
:key="index + 'date'"
class="d-flex justify-center mouse"
>
<div
class="d-inline px-2 py-0 caption text-center mb-2 rounded-lg background grey--text"
>
{{ new Date(reply.createdAt).toLocaleString() }} {{ new Date(reply.createdAt).toLocaleString() }}
<timeago :datetime="reply.createdAt" class="font-italic ma-1"></timeago> <timeago :datetime="reply.createdAt" class="font-italic ma-1"></timeago>
</div> </div>
@@ -52,11 +69,14 @@
</template> </template>
<div v-if="$loggedIn()" class="px-0 mb-4"> <div v-if="$loggedIn()" class="px-0 mb-4">
<v-slide-y-transition> <v-slide-y-transition>
<div class="px-4 py-2 caption mb-2 background rounded-xl" v-show="whoIsTyping.length > 0"> <div
{{typingStatusText}} class="px-4 py-2 caption mb-2 background rounded-xl"
v-show="whoIsTyping.length > 0"
>
{{ typingStatusText }}
</div> </div>
</v-slide-y-transition> </v-slide-y-transition>
<div > <div>
<v-textarea <v-textarea
:disabled="loadingReply" :disabled="loadingReply"
v-model="replyText" v-model="replyText"
@@ -73,7 +93,7 @@
></v-textarea> ></v-textarea>
</div> </div>
<div class="px-2" v-show="loadingReply"> <div class="px-2" v-show="loadingReply">
<v-progress-linear indeterminate/> <v-progress-linear indeterminate />
</div> </div>
<div class="text-right" ref="replyinput"> <div class="text-right" ref="replyinput">
<v-btn <v-btn
@@ -108,7 +128,9 @@
</v-app-bar-nav-icon> </v-app-bar-nav-icon>
<v-toolbar-title>Archive Comment Thread</v-toolbar-title> <v-toolbar-title>Archive Comment Thread</v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon @click="showArchiveDialog = false"><v-icon>mdi-close</v-icon></v-btn> <v-btn icon @click="showArchiveDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar> </v-toolbar>
<v-card-text class="mt-4"> <v-card-text class="mt-4">
This comment thread will be archived. Are you sure? This comment thread will be archived. Are you sure?
@@ -122,7 +144,13 @@
</v-dialog> </v-dialog>
</div> </div>
<div v-else> <div v-else>
<v-btn block depressed color="primary" class="rounded-xl" @click="$loginAndSetRedirect()"> <v-btn
block
depressed
color="primary"
class="rounded-xl"
@click="$loginAndSetRedirect()"
>
<v-icon small class="mr-1">mdi-account</v-icon> <v-icon small class="mr-1">mdi-account</v-icon>
Sign in to reply Sign in to reply
</v-btn> </v-btn>
@@ -144,7 +172,7 @@ export default {
apollo: { apollo: {
user: { user: {
query: gql` query: gql`
query{ query {
user { user {
name name
id id
@@ -154,8 +182,8 @@ export default {
}, },
stream: { stream: {
query: gql` query: gql`
query($streamId: String!) { query ($streamId: String!) {
stream(id: $streamId){ stream(id: $streamId) {
id id
role role
} }
@@ -167,7 +195,7 @@ export default {
}, },
replyQuery: { replyQuery: {
query: gql` query: gql`
query($streamId: String!, $id: String!) { query ($streamId: String!, $id: String!) {
comment(streamId: $streamId, id: $id) { comment(streamId: $streamId, id: $id) {
id id
viewedAt viewedAt
@@ -193,7 +221,7 @@ export default {
} }
}, },
result({ data }) { result({ data }) {
if(!data) return if (!data) return
data.comment.replies.items.forEach((item) => { data.comment.replies.items.forEach((item) => {
if (this.localReplies.findIndex((c) => c.id === item.id) === -1) if (this.localReplies.findIndex((c) => c.id === item.id) === -1)
this.localReplies.push(item) this.localReplies.push(item)
@@ -205,7 +233,7 @@ export default {
$subscribe: { $subscribe: {
commentThreadActivity: { commentThreadActivity: {
query: gql` query: gql`
subscription($streamId: String!, $commentId: String!) { subscription ($streamId: String!, $commentId: String!) {
commentThreadActivity(streamId: $streamId, commentId: $commentId) commentThreadActivity(streamId: $streamId, commentId: $commentId)
} }
`, `,
@@ -219,7 +247,7 @@ export default {
return !this.$loggedIn() return !this.$loggedIn()
}, },
result({ data }) { result({ data }) {
if(!data || !data.commentThreadActivity) return if (!data || !data.commentThreadActivity) return
if (data.commentThreadActivity.eventType === 'reply-added') { if (data.commentThreadActivity.eventType === 'reply-added') {
if (!this.comment.expanded) return this.$emit('bounce', this.comment.id) if (!this.comment.expanded) return this.$emit('bounce', this.comment.id)
else { else {
@@ -228,26 +256,26 @@ export default {
}, 100) }, 100)
} }
this.localReplies.push({ ...data.commentThreadActivity }) this.localReplies.push({ ...data.commentThreadActivity })
this.$refs.replyinput.scrollIntoView({behaviour: 'smooth', block: 'end' }) this.$refs.replyinput.scrollIntoView({ behaviour: 'smooth', block: 'end' })
return return
} }
if (data.commentThreadActivity.eventType === 'comment-archived') { if (data.commentThreadActivity.eventType === 'comment-archived') {
this.$emit('deleted', this.comment) this.$emit('deleted', this.comment)
} }
if(data.commentThreadActivity.eventType === 'reply-typing-status') { if (data.commentThreadActivity.eventType === 'reply-typing-status') {
let state = data.commentThreadActivity.data let state = data.commentThreadActivity.data
if(state.userId === this.$userId()) return if (state.userId === this.$userId()) return
let existingUser = this.whoIsTyping.find( u => u.userId === state.userId) let existingUser = this.whoIsTyping.find((u) => u.userId === state.userId)
if(state.isTyping && existingUser) { if (state.isTyping && existingUser) {
existingUser.lastSeenAt = Date.now() existingUser.lastSeenAt = Date.now()
return return
} }
if(!state.isTyping) { if (!state.isTyping) {
let indx = this.whoIsTyping.findIndex( u => u.userId === state.userId) let indx = this.whoIsTyping.findIndex((u) => u.userId === state.userId)
if(indx!==-1) this.whoIsTyping.splice(indx, 1) if (indx !== -1) this.whoIsTyping.splice(indx, 1)
return return
} }
if(state.isTyping && !existingUser) { if (state.isTyping && !existingUser) {
state.lastSeenAt = Date.now() state.lastSeenAt = Date.now()
this.whoIsTyping.push(state) this.whoIsTyping.push(state)
} }
@@ -269,9 +297,13 @@ export default {
}, },
computed: { computed: {
canArchiveThread() { canArchiveThread() {
if(!this.comment || !this.stream ) return false if (!this.comment || !this.stream) return false
if(!this.stream.role) return false if (!this.stream.role) return false
if(this.comment.authorId === this.$userId() || this.stream.role ==='stream:owner') return true if (
this.comment.authorId === this.$userId() ||
this.stream.role === 'stream:owner'
)
return true
}, },
thread() { thread() {
let sorted = [...this.localReplies].sort( let sorted = [...this.localReplies].sort(
@@ -302,13 +334,13 @@ export default {
return route return route
}, },
typingStatusText() { typingStatusText() {
if(this.whoIsTyping.length === 0) return null if (this.whoIsTyping.length === 0) return null
if(this.whoIsTyping.length > 1) { if (this.whoIsTyping.length > 1) {
return `${this.whoIsTyping.map(u=>u.userName).join(', ')} are typing...` return `${this.whoIsTyping.map((u) => u.userName).join(', ')} are typing...`
} else { } else {
return `${this.whoIsTyping[0].userName} is typing...` return `${this.whoIsTyping[0].userName} is typing...`
} }
}, }
}, },
watch: { watch: {
'comment.expanded': { 'comment.expanded': {
@@ -320,7 +352,10 @@ export default {
commentView(streamId: $streamId, commentId: $commentId) commentView(streamId: $streamId, commentId: $commentId)
} }
`, `,
variables: { streamId: this.$route.params.streamId, commentId: this.comment.id } variables: {
streamId: this.$route.params.streamId,
commentId: this.comment.id
}
}) })
// eslint-disable-next-line vue/no-mutating-props // eslint-disable-next-line vue/no-mutating-props
@@ -332,33 +367,42 @@ export default {
} }
} }
}, },
mounted(){ mounted() {
window.addEventListener('beforeunload', async (e) => { window.addEventListener('beforeunload', async (e) => {
await this.sendTypingUpdate( false ) await this.sendTypingUpdate(false)
}) })
setInterval(()=>{ setInterval(() => {
let now = Date.now() let now = Date.now()
for(let i = this.whoIsTyping.length-1; i >= 0; i--) { for (let i = this.whoIsTyping.length - 1; i >= 0; i--) {
if(Math.abs(now - this.whoIsTyping[i].lastSeenAt) > 10000) this.whoIsTyping.splice(i, 1) if (Math.abs(now - this.whoIsTyping[i].lastSeenAt) > 10000)
this.whoIsTyping.splice(i, 1)
} }
}, 5000) }, 5000)
}, },
methods: { methods: {
debTypingUpdate: debounce( async function() { debTypingUpdate: debounce(
if(!this.$loggedIn()) return async function () {
if (!this.$loggedIn()) return
await this.sendTypingUpdate(this.isTyping) await this.sendTypingUpdate(this.isTyping)
this.isTyping = !this.isTyping this.isTyping = !this.isTyping
}, 7000, { leading: true }), },
7000,
{ leading: true }
),
async sendTypingUpdate( state = true) { async sendTypingUpdate(state = true) {
if(!this.$loggedIn()) return if (!this.$loggedIn()) return
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: gql` mutation: gql`
mutation typingUpdate($sId: String!, $cId: String!, $d: JSONObject ) { mutation typingUpdate($sId: String!, $cId: String!, $d: JSONObject) {
userCommentThreadActivityBroadcast(streamId: $sId, commentId: $cId, data: $d) userCommentThreadActivityBroadcast(
streamId: $sId
commentId: $cId
data: $d
)
} }
`, `,
variables:{ variables: {
sId: this.$route.params.streamId, sId: this.$route.params.streamId,
cId: this.comment.id, cId: this.comment.id,
d: { d: {
@@ -428,7 +472,7 @@ export default {
`, `,
variables: { input: replyInput } variables: { input: replyInput }
}) })
await this.sendTypingUpdate( false ) await this.sendTypingUpdate(false)
this.$mixpanel.track('Comment Action', { type: 'action', name: 'reply' }) this.$mixpanel.track('Comment Action', { type: 'action', name: 'reply' })
} catch (e) { } catch (e) {
this.$eventHub.$emit('notification', { this.$eventHub.$emit('notification', {
@@ -2,7 +2,9 @@
<div> <div>
<v-list dense nav class="mt-4 py-0 mb-3"> <v-list dense nav class="mt-4 py-0 mb-3">
<v-list-item <v-list-item
:class="`px-2 list-overlay-${$vuetify.theme.dark ? 'dark' : 'light'} elevation-2`" :class="`px-2 list-overlay-${
$vuetify.theme.dark ? 'dark' : 'light'
} elevation-2`"
style="position: sticky; top: 82px" style="position: sticky; top: 82px"
@click="expand = !expand" @click="expand = !expand"
> >
@@ -29,7 +31,8 @@
:key="comment.id + '-card-sidebar'" :key="comment.id + '-card-sidebar'"
no-gutters no-gutters
:class="`${isUnread(comment) ? 'border' : ''} my-2 property-row rounded-lg ${ :class="`${isUnread(comment) ? 'border' : ''} my-2 property-row rounded-lg ${
$store.state.selectedComment && $store.state.selectedComment.id === comment.id $store.state.selectedComment &&
$store.state.selectedComment.id === comment.id
? 'elevation-5 selected' ? 'elevation-5 selected'
: '' : ''
}`" }`"
@@ -40,7 +43,9 @@
</v-col> </v-col>
<v-col <v-col
cols="8" cols="8"
:class="`pl-2 body-2 text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`" :class="`pl-2 body-2 text-truncate px-1 ${
$vuetify.theme.dark ? 'grey--text' : ''
}`"
style="line-height: 30px" style="line-height: 30px"
> >
{{ comment.text }} {{ comment.text }}
@@ -70,7 +75,14 @@
<timeago :datetime="comment.updatedAt" /> <timeago :datetime="comment.updatedAt" />
</v-col> </v-col>
</v-row> </v-row>
<v-btn small block class="rounded-xl" :to="`/streams/${$route.params.streamId}/comments`">all stream comments</v-btn> <v-btn
small
block
class="rounded-xl"
:to="`/streams/${$route.params.streamId}/comments`"
>
all stream comments
</v-btn>
</div> </div>
</v-scroll-y-transition> </v-scroll-y-transition>
</div> </div>
@@ -33,7 +33,7 @@
</div> </div>
</v-card-text> </v-card-text>
</div> </div>
<v-divider v-if="showStreamAndBranch"/> <v-divider v-if="showStreamAndBranch" />
<div v-if="showStreamAndBranch" class="d-flex align-center caption px-5 py-2"> <div v-if="showStreamAndBranch" class="d-flex align-center caption px-5 py-2">
<div class="text-truncate mr-2"> <div class="text-truncate mr-2">
<router-link <router-link
@@ -58,8 +58,17 @@
<commit-received-receipts :stream-id="streamId" :commit-id="commit.id" shadow /> <commit-received-receipts :stream-id="streamId" :commit-id="commit.id" shadow />
</div> </div>
<div style="position: absolute; top: 10px; left: 12px"> <div style="position: absolute; top: 10px; left: 12px">
<v-chip v-if="commit.commentCount !== 0" small class="caption primary" dark v-tooltip="`${commit.commentCount} comment${commit.commentCount === 1 ? '' : 's'}`"> <v-chip
<v-icon x-small class="mr-1">mdi-comment-outline</v-icon> {{ commit.commentCount }} v-if="commit.commentCount !== 0"
small
class="caption primary"
dark
v-tooltip="
`${commit.commentCount} comment${commit.commentCount === 1 ? '' : 's'}`
"
>
<v-icon x-small class="mr-1">mdi-comment-outline</v-icon>
{{ commit.commentCount }}
</v-chip> </v-chip>
<source-app-avatar :application-name="commit.sourceApplication" /> <source-app-avatar :application-name="commit.sourceApplication" />
</div> </div>
@@ -70,7 +79,8 @@
export default { export default {
components: { components: {
PreviewImage: () => import('@/main/components/common/PreviewImage'), PreviewImage: () => import('@/main/components/common/PreviewImage'),
CommitReceivedReceipts: () => import('@/main/components/common/CommitReceivedReceipts'), CommitReceivedReceipts: () =>
import('@/main/components/common/CommitReceivedReceipts'),
SourceAppAvatar: () => import('@/main/components/common/SourceAppAvatar') SourceAppAvatar: () => import('@/main/components/common/SourceAppAvatar')
}, },
props: { props: {
@@ -78,7 +88,7 @@ export default {
previewHeight: { type: Number, default: () => 180 }, previewHeight: { type: Number, default: () => 180 },
showStreamAndBranch: { type: Boolean, default: true } showStreamAndBranch: { type: Boolean, default: true }
}, },
computed:{ computed: {
streamId() { streamId() {
return this.commit.streamId ?? this.$route.params.streamId return this.commit.streamId ?? this.$route.params.streamId
} }
@@ -47,7 +47,9 @@
<v-app-bar-nav-icon><v-icon>mdi-download</v-icon></v-app-bar-nav-icon> <v-app-bar-nav-icon><v-icon>mdi-download</v-icon></v-app-bar-nav-icon>
<v-toolbar-title>All Received Receipts</v-toolbar-title> <v-toolbar-title>All Received Receipts</v-toolbar-title>
<v-spacer /> <v-spacer />
<v-btn icon @click="showAllActivityDialog = false"><v-icon>mdi-close</v-icon></v-btn> <v-btn icon @click="showAllActivityDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar> </v-toolbar>
<v-list> <v-list>
<v-list-item v-for="(act, i) in activity.items" :key="i"> <v-list-item v-for="(act, i) in activity.items" :key="i">
@@ -63,7 +65,10 @@
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<source-app-avatar class="mt-3 mb-3" :application-name="act.info.sourceApplication" /> <source-app-avatar
class="mt-3 mb-3"
:application-name="act.info.sourceApplication"
/>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
</v-list> </v-list>
@@ -131,7 +136,8 @@ export default {
}, },
computed: { computed: {
receivedUsersUnique() { receivedUsersUnique() {
if (!(this.activity && this.activity.items && this.activity.items.length > 0)) return [] if (!(this.activity && this.activity.items && this.activity.items.length > 0))
return []
let set = new Set() let set = new Set()
this.activity.items.forEach((item) => set.add(item.userId)) this.activity.items.forEach((item) => set.add(item.userId))
return Array.from(set) return Array.from(set)
@@ -1,10 +1,21 @@
<template> <template>
<v-container> <v-container>
<v-row justify="center" style="margin-top: 50px" dense> <v-row justify="center" style="margin-top: 50px" dense>
<v-col cols="12" lg="6" md="6" xl="6" class="d-flex flex-column justify-center align-center"> <v-col
cols="12"
lg="6"
md="6"
xl="6"
class="d-flex flex-column justify-center align-center"
>
<v-card flat tile color="transparent" class="pa-0"> <v-card flat tile color="transparent" class="pa-0">
<div class="d-flex flex-column justify-space-between align-center mb-10"> <div class="d-flex flex-column justify-space-between align-center mb-10">
<v-img v-if="!errorType" contain max-height="200" src="@/assets/emptybox.png"></v-img> <v-img
v-if="!errorType"
contain
max-height="200"
src="@/assets/emptybox.png"
></v-img>
<v-img <v-img
v-else-if="errorType == 'access'" v-else-if="errorType == 'access'"
contain contain
@@ -29,7 +40,9 @@
class="primary mb-4 no-overlay" class="primary mb-4 no-overlay"
dark dark
:to="`${ :to="`${
$route.params.streamId && errorType !== '404' && errorType !== 'access' $route.params.streamId &&
errorType !== '404' &&
errorType !== 'access'
? '/streams/' + $route.params.streamId ? '/streams/' + $route.params.streamId
: '/' : '/'
}`" }`"
@@ -39,7 +52,9 @@
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Home</v-list-item-title> <v-list-item-title>Home</v-list-item-title>
<v-list-item-subtitle class="caption">Go to the homepage</v-list-item-subtitle> <v-list-item-subtitle class="caption">
Go to the homepage
</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>
@@ -1,9 +1,18 @@
<template> <template>
<v-container> <v-container>
<v-row justify="center" style="margin-top: 50px" dense> <v-row justify="center" style="margin-top: 50px" dense>
<v-col cols="12" lg="6" md="6" xl="6" class="d-flex flex-column justify-center align-center"> <v-col
cols="12"
lg="6"
md="6"
xl="6"
class="d-flex flex-column justify-center align-center"
>
<v-card flat tile color="transparent" class="pa-0"> <v-card flat tile color="transparent" class="pa-0">
<div v-if="showImage" class="d-flex flex-column justify-space-between align-center mb-10"> <div
v-if="showImage"
class="d-flex flex-column justify-space-between align-center mb-10"
>
<v-img contain max-height="200" src="@/assets/emptybox.png"></v-img> <v-img contain max-height="200" src="@/assets/emptybox.png"></v-img>
</div> </div>
<div class="text-center mb-2 space-grotesk"> <div class="text-center mb-2 space-grotesk">
@@ -25,8 +34,8 @@
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Install Connectors</v-list-item-title> <v-list-item-title>Install Connectors</v-list-item-title>
<p class="caption pb-0 mb-0"> <p class="caption pb-0 mb-0">
Download Speckle Manager to install connectors for your design applications Download Speckle Manager to install connectors for your design
and start sending data in no time! applications and start sending data in no time!
</p> </p>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
@@ -42,7 +51,8 @@
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Authenticate</v-list-item-title> <v-list-item-title>Authenticate</v-list-item-title>
<p class="caption pb-0 mb-0"> <p class="caption pb-0 mb-0">
Link up your Speckle account with the desktop connectors you have installed. Link up your Speckle account with the desktop connectors you have
installed.
</p> </p>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
@@ -32,7 +32,8 @@
<v-list-item-content> <v-list-item-content>
<v-list-item-title> <v-list-item-title>
<div class="text-subtitle-1 text-truncate"> <div class="text-subtitle-1 text-truncate">
Nothing found. Please search again (your query has to be longer than 3 charact) Nothing found. Please search again (your query has to be longer than 3
charact)
</div> </div>
</v-list-item-title> </v-list-item-title>
</v-list-item-content> </v-list-item-content>
@@ -1,6 +1,12 @@
<template> <template>
<v-card :class="`elevation-${elevation} rounded-lg overflow-hidden ${funky ? 'funky' : ''}`"> <v-card
<v-toolbar v-show="hasHeaderSlot || hasActionsSlot || expandable" flat :dense="dense"> :class="`elevation-${elevation} rounded-lg overflow-hidden ${funky ? 'funky' : ''}`"
>
<v-toolbar
v-show="hasHeaderSlot || hasActionsSlot || expandable"
flat
:dense="dense"
>
<v-toolbar-title class="text-subtitle-1"> <v-toolbar-title class="text-subtitle-1">
<slot name="header"></slot> <slot name="header"></slot>
</v-toolbar-title> </v-toolbar-title>
@@ -1,6 +1,10 @@
<template> <template>
<v-hover v-slot="{ hover }"> <v-hover v-slot="{ hover }">
<v-card class="rounded-lg" :elevation="hover ? 10 : 1" style="transition: all 0.2s ease-in-out"> <v-card
class="rounded-lg"
:elevation="hover ? 10 : 1"
style="transition: all 0.2s ease-in-out"
>
<router-link :to="`/streams/${stream.id}`"> <router-link :to="`/streams/${stream.id}`">
<preview-image <preview-image
:url="`/preview/${stream.id}`" :url="`/preview/${stream.id}`"
@@ -35,8 +39,18 @@
</div> </div>
</v-card-text> </v-card-text>
<div style="position: absolute; top: 10px; left: 12px"> <div style="position: absolute; top: 10px; left: 12px">
<v-chip :to="`/streams/${stream.id}/comments`" v-if="stream.commentCount !== 0" small class="caption primary" dark v-tooltip="`${stream.commentCount} comment${stream.commentCount === 1 ? '' : 's'}`"> <v-chip
<v-icon x-small class="mr-1">mdi-comment-outline</v-icon> {{ stream.commentCount }} :to="`/streams/${stream.id}/comments`"
v-if="stream.commentCount !== 0"
small
class="caption primary"
dark
v-tooltip="
`${stream.commentCount} comment${stream.commentCount === 1 ? '' : 's'}`
"
>
<v-icon x-small class="mr-1">mdi-comment-outline</v-icon>
{{ stream.commentCount }}
</v-chip> </v-chip>
</div> </div>
<v-divider /> <v-divider />
@@ -54,7 +68,9 @@
> >
<v-icon <v-icon
small small
:class="`mr-1 ${stream.role.split(':')[1] === 'owner' ? 'primary--text' : ''}`" :class="`mr-1 ${
stream.role.split(':')[1] === 'owner' ? 'primary--text' : ''
}`"
> >
mdi-account-key-outline mdi-account-key-outline
</v-icon> </v-icon>
@@ -69,7 +85,8 @@ export default {
components: { components: {
PreviewImage: () => import('@/main/components/common/PreviewImage.vue'), PreviewImage: () => import('@/main/components/common/PreviewImage.vue'),
CollaboratorsDisplay: () => import('@/main/components/stream/CollaboratorsDisplay'), CollaboratorsDisplay: () => import('@/main/components/stream/CollaboratorsDisplay'),
StreamFavoriteBtn: () => import('@/main/components/stream/favorites/StreamFavoriteBtn.vue') StreamFavoriteBtn: () =>
import('@/main/components/stream/favorites/StreamFavoriteBtn.vue')
}, },
props: { props: {
stream: { type: Object, default: () => null }, stream: { type: Object, default: () => null },
@@ -46,7 +46,9 @@
</div> </div>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<v-btn x-small block :to="isSelf ? '/profile' : '/profile/' + id">View profile</v-btn> <v-btn x-small block :to="isSelf ? '/profile' : '/profile/' + id">
View profile
</v-btn>
</div> </div>
</v-card-text> </v-card-text>
</v-card> </v-card>
@@ -1,6 +1,10 @@
<template> <template>
<v-navigation-drawer app right fixed class="transparent overflow-auto" floating> <v-navigation-drawer app right fixed class="transparent overflow-auto" floating>
<v-card rounded="lg" style="overflow: hidden" class="transparent elevation-0 pl-1 pr-3 pb-4"> <v-card
rounded="lg"
style="overflow: hidden"
class="transparent elevation-0 pl-1 pr-3 pb-4"
>
<v-toolbar class="mt-3" rounded="lg" dense> <v-toolbar class="mt-3" rounded="lg" dense>
<v-toolbar-title class="body-2 font-weight-bold"> <v-toolbar-title class="body-2 font-weight-bold">
<a <a
@@ -12,7 +16,10 @@
</a> </a>
</v-toolbar-title> </v-toolbar-title>
<v-spacer /> <v-spacer />
<v-app-bar-nav-icon href="https://github.com/specklesystems/speckle-server" target="_blank"> <v-app-bar-nav-icon
href="https://github.com/specklesystems/speckle-server"
target="_blank"
>
<v-icon small class="yellow--text">mdi-star</v-icon> <v-icon small class="yellow--text">mdi-star</v-icon>
</v-app-bar-nav-icon> </v-app-bar-nav-icon>
</v-toolbar> </v-toolbar>
@@ -56,7 +63,11 @@
</div> </div>
<v-toolbar class="my-4" rounded="lg" dense flat> <v-toolbar class="my-4" rounded="lg" dense flat>
<v-toolbar-title class="body-2"> <v-toolbar-title class="body-2">
<a href="https://speckle.systems/tutorials" target="_blank" class="text-decoration-none"> <a
href="https://speckle.systems/tutorials"
target="_blank"
class="text-decoration-none"
>
More Tutorials More Tutorials
</a> </a>
</v-toolbar-title> </v-toolbar-title>
@@ -68,12 +79,20 @@
<v-card-text class="caption"> <v-card-text class="caption">
<p class="mb-0"> <p class="mb-0">
At At
<a href="https://speckle.systems" target="_blank" class="text-decoration-none"> <a
href="https://speckle.systems"
target="_blank"
class="text-decoration-none"
>
Speckle Speckle
</a> </a>
we're working tirelessly to bring you the best open source data platform for AEC. Tell we're working tirelessly to bring you the best open source data platform for
us what you think on our AEC. Tell us what you think on our
<a href="https://speckle.community" target="_blank" class="text-decoration-none"> <a
href="https://speckle.community"
target="_blank"
class="text-decoration-none"
>
forum forum
</a> </a>
, and don't forget to give us a on , and don't forget to give us a on
@@ -36,11 +36,16 @@
class="mr-3" class="mr-3"
:user-id="activityItem.info.targetUser" :user-id="activityItem.info.targetUser"
:color=" :color="
lastActivity.actionType === 'stream_permissions_add' ? 'success' : 'error' lastActivity.actionType === 'stream_permissions_add'
? 'success'
: 'error'
" "
></user-pill> ></user-pill>
<span v-if="$vuetify.breakpoint.smAndUp" class="mr-3 body-2 font-italic"> <span
v-if="$vuetify.breakpoint.smAndUp"
class="mr-3 body-2 font-italic"
>
{{ {{
lastActivity.actionType === 'stream_permissions_add' lastActivity.actionType === 'stream_permissions_add'
? 'user added as' ? 'user added as'
@@ -85,7 +90,9 @@
<v-icon color="primary" small>mdi-folder</v-icon> <v-icon color="primary" small>mdi-folder</v-icon>
{{ stream.name }} {{ stream.name }}
</router-link> </router-link>
<span class="ml-3 body-2 font-italic">{{ lastActivityBrief.actionText }}</span> <span class="ml-3 body-2 font-italic">
{{ lastActivityBrief.actionText }}
</span>
<v-spacer /> <v-spacer />
@@ -168,10 +175,14 @@
> >
<v-card-text class="pa-5 body-1"> <v-card-text class="pa-5 body-1">
<v-chip :to="url" :color="lastActivityBrief.color"> <v-chip :to="url" :color="lastActivityBrief.color">
<v-icon small class="mr-2 float-left" light>{{ lastActivityBrief.icon }}</v-icon> <v-icon small class="mr-2 float-left" light>
{{ lastActivityBrief.icon }}
</v-icon>
{{ branchName }} {{ branchName }}
</v-chip> </v-chip>
<span class="ml-3 body-2 font-italic">{{ lastActivityBrief.actionText }}</span> <span class="ml-3 body-2 font-italic">
{{ lastActivityBrief.actionText }}
</span>
<div class="mt-3"> <div class="mt-3">
<div <div
v-for="activityItem in activityGroup" v-for="activityItem in activityGroup"
@@ -215,7 +226,9 @@
</v-chip> </v-chip>
<span v-if="lastActivity.actionType === 'commit_create'"> <span v-if="lastActivity.actionType === 'commit_create'">
<span class="mx-3 body-2 font-italic">from</span> <span class="mx-3 body-2 font-italic">from</span>
<source-app-avatar :application-name="commit.sourceApplication" /> <source-app-avatar
:application-name="commit.sourceApplication"
/>
</span> </span>
<span v-if="lastActivity.actionType === 'commit_receive'"> <span v-if="lastActivity.actionType === 'commit_receive'">
<span class="mx-3 body-2 font-italic">in</span> <span class="mx-3 body-2 font-italic">in</span>
@@ -299,7 +312,7 @@ export default {
}, },
user: { user: {
query: gql` query: gql`
query($id: String) { query ($id: String) {
user(id: $id) { user(id: $id) {
name name
avatar avatar
@@ -316,7 +329,7 @@ export default {
stream: { stream: {
query: gql` query: gql`
query($id: String!) { query ($id: String!) {
stream(id: $id) { stream(id: $id) {
id id
name name
@@ -339,7 +352,7 @@ export default {
}, },
branch: { branch: {
query: gql` query: gql`
query($id: String!, $branchName: String!) { query ($id: String!, $branchName: String!) {
stream(id: $id) { stream(id: $id) {
id id
branch(name: $branchName) { branch(name: $branchName) {
@@ -361,7 +374,7 @@ export default {
}, },
commit: { commit: {
query: gql` query: gql`
query($id: String!, $commitId: String!) { query ($id: String!, $commitId: String!) {
stream(id: $id) { stream(id: $id) {
id id
commit(id: $commitId) { commit(id: $commitId) {
@@ -438,13 +451,17 @@ export default {
case 'stream_permissions_add': case 'stream_permissions_add':
return { return {
captionText: `added ${ captionText: `added ${
this.activityGroup.length === 1 ? 'a user' : this.activityGroup.length + ' users' this.activityGroup.length === 1
? 'a user'
: this.activityGroup.length + ' users'
} to` } to`
} }
case 'stream_permissions_remove': case 'stream_permissions_remove':
return { return {
captionText: `removed ${ captionText: `removed ${
this.activityGroup.length === 1 ? 'a user' : this.activityGroup.length + ' users' this.activityGroup.length === 1
? 'a user'
: this.activityGroup.length + ' users'
} from` } from`
} }
case 'branch_create': case 'branch_create':
@@ -36,11 +36,16 @@
class="mr-3" class="mr-3"
:user-id="activityItem.info.targetUser" :user-id="activityItem.info.targetUser"
:color=" :color="
lastActivity.actionType === 'stream_permissions_add' ? 'success' : 'error' lastActivity.actionType === 'stream_permissions_add'
? 'success'
: 'error'
" "
></user-pill> ></user-pill>
<span v-if="$vuetify.breakpoint.smAndUp" class="mr-3 body-2 font-italic"> <span
v-if="$vuetify.breakpoint.smAndUp"
class="mr-3 body-2 font-italic"
>
{{ {{
lastActivity.actionType === 'stream_permissions_add' lastActivity.actionType === 'stream_permissions_add'
? 'user added as' ? 'user added as'
@@ -85,7 +90,9 @@
<v-icon color="primary" small>mdi-folder</v-icon> <v-icon color="primary" small>mdi-folder</v-icon>
{{ stream.name }} {{ stream.name }}
</router-link> </router-link>
<span class="ml-3 caption font-italic">{{ lastActivityBrief.actionText }}</span> <span class="ml-3 caption font-italic">
{{ lastActivityBrief.actionText }}
</span>
<v-spacer /> <v-spacer />
@@ -168,10 +175,14 @@
> >
<v-card-text class="xxxpa-5 body-1"> <v-card-text class="xxxpa-5 body-1">
<v-chip :to="url" :color="lastActivityBrief.color"> <v-chip :to="url" :color="lastActivityBrief.color">
<v-icon small class="mr-2 float-left" light>{{ lastActivityBrief.icon }}</v-icon> <v-icon small class="mr-2 float-left" light>
{{ lastActivityBrief.icon }}
</v-icon>
{{ branchName }} {{ branchName }}
</v-chip> </v-chip>
<span class="ml-3 body-2 font-italic">{{ lastActivityBrief.actionText }}</span> <span class="ml-3 body-2 font-italic">
{{ lastActivityBrief.actionText }}
</span>
<div class="mt-3"> <div class="mt-3">
<div <div
v-for="activityItem in activityGroup" v-for="activityItem in activityGroup"
@@ -215,7 +226,9 @@
</v-chip> </v-chip>
<span v-if="lastActivity.actionType === 'commit_create'"> <span v-if="lastActivity.actionType === 'commit_create'">
<span class="mx-3 body-2 font-italic">from</span> <span class="mx-3 body-2 font-italic">from</span>
<source-app-avatar :application-name="commit.sourceApplication" /> <source-app-avatar
:application-name="commit.sourceApplication"
/>
</span> </span>
<span v-if="lastActivity.actionType === 'commit_receive'"> <span v-if="lastActivity.actionType === 'commit_receive'">
<span class="mx-3 body-2 font-italic">in</span> <span class="mx-3 body-2 font-italic">in</span>
@@ -299,7 +312,7 @@ export default {
}, },
user: { user: {
query: gql` query: gql`
query($id: String) { query ($id: String) {
user(id: $id) { user(id: $id) {
name name
avatar avatar
@@ -316,7 +329,7 @@ export default {
stream: { stream: {
query: gql` query: gql`
query($id: String!) { query ($id: String!) {
stream(id: $id) { stream(id: $id) {
id id
name name
@@ -339,7 +352,7 @@ export default {
}, },
branch: { branch: {
query: gql` query: gql`
query($id: String!, $branchName: String!) { query ($id: String!, $branchName: String!) {
stream(id: $id) { stream(id: $id) {
id id
branch(name: $branchName) { branch(name: $branchName) {
@@ -361,7 +374,7 @@ export default {
}, },
commit: { commit: {
query: gql` query: gql`
query($id: String!, $commitId: String!) { query ($id: String!, $commitId: String!) {
stream(id: $id) { stream(id: $id) {
id id
commit(id: $commitId) { commit(id: $commitId) {
@@ -438,13 +451,17 @@ export default {
case 'stream_permissions_add': case 'stream_permissions_add':
return { return {
captionText: `added ${ captionText: `added ${
this.activityGroup.length === 1 ? 'a user' : this.activityGroup.length + ' users' this.activityGroup.length === 1
? 'a user'
: this.activityGroup.length + ' users'
} to` } to`
} }
case 'stream_permissions_remove': case 'stream_permissions_remove':
return { return {
captionText: `removed ${ captionText: `removed ${
this.activityGroup.length === 1 ? 'a user' : this.activityGroup.length + ' users' this.activityGroup.length === 1
? 'a user'
: this.activityGroup.length + ' users'
} from` } from`
} }
case 'branch_create': case 'branch_create':
@@ -40,7 +40,8 @@
<no-data-placeholder v-if="quickUser"> <no-data-placeholder v-if="quickUser">
<h2>Welcome {{ quickUser.name.split(' ')[0] }}!</h2> <h2>Welcome {{ quickUser.name.split(' ')[0] }}!</h2>
<p class="caption"> <p class="caption">
Once you create a stream and start sending some data, your activity will show up here. Once you create a stream and start sending some data, your activity will show
up here.
</p> </p>
<template #actions> <template #actions>
@@ -100,7 +101,7 @@ export default {
}, },
timeline: { timeline: {
query: gql` query: gql`
query($cursor: DateTime) { query ($cursor: DateTime) {
user { user {
id id
timeline(cursor: $cursor) { timeline(cursor: $cursor) {
@@ -173,7 +174,11 @@ export default {
if (curr.actionType === test.actionType && curr.streamId === test.streamId) { if (curr.actionType === test.actionType && curr.streamId === test.streamId) {
if (curr.actionType.includes('stream_permissions')) { if (curr.actionType.includes('stream_permissions')) {
//skip multiple stream_permission actions on the same user, just pick the last! //skip multiple stream_permission actions on the same user, just pick the last!
if (prev[prev.length - 1].some((x) => x.info.targetUser === curr.info.targetUser)) if (
prev[prev.length - 1].some(
(x) => x.info.targetUser === curr.info.targetUser
)
)
action = 'skip' action = 'skip'
else action = 'combine' else action = 'combine'
} //stream, branch, commit } //stream, branch, commit
@@ -208,7 +213,10 @@ export default {
if (newItems.length === 0) $state.complete() if (newItems.length === 0) $state.complete()
else $state.loaded() else $state.loaded()
fetchMoreResult.user.timeline.items = [...previousResult.user.timeline.items, ...newItems] fetchMoreResult.user.timeline.items = [
...previousResult.user.timeline.items,
...newItems
]
return fetchMoreResult return fetchMoreResult
} }
@@ -14,8 +14,16 @@
<v-window v-model="onboarding" class="pb-3"> <v-window v-model="onboarding" class="pb-3">
<v-window-item> <v-window-item>
<v-card class="transparent elevation-0 text-center" color="transparent" align="start"> <v-card
<v-img class="align-start mb-3" src="@/assets/onboarding-1.png" :aspect-ratio="16 / 9"> class="transparent elevation-0 text-center"
color="transparent"
align="start"
>
<v-img
class="align-start mb-3"
src="@/assets/onboarding-1.png"
:aspect-ratio="16 / 9"
>
<template #sources> <template #sources>
<source srcset="@/assets/onboarding-1.webp" /> <source srcset="@/assets/onboarding-1.webp" />
</template> </template>
@@ -32,8 +40,8 @@
<b>plugins</b> <b>plugins</b>
- our speckle - our speckle
<b>connectors</b> <b>connectors</b>
- to help you extract your 3D objects and all their properties from your - to help you extract your 3D objects and all their properties
desktop application. from your desktop application.
</p> </p>
</v-col> </v-col>
</v-row> </v-row>
@@ -43,8 +51,16 @@
</v-window-item> </v-window-item>
<v-window-item> <v-window-item>
<v-card class="transparent elevation-0 text-center" color="transparent" align="start"> <v-card
<v-img class="align-start mb-3" src="@/assets/onboarding-2.png" :aspect-ratio="16 / 9"> class="transparent elevation-0 text-center"
color="transparent"
align="start"
>
<v-img
class="align-start mb-3"
src="@/assets/onboarding-2.png"
:aspect-ratio="16 / 9"
>
<template #sources> <template #sources>
<source srcset="@/assets/onboarding-2.webp" /> <source srcset="@/assets/onboarding-2.webp" />
</template> </template>
@@ -57,10 +73,11 @@
<v-row> <v-row>
<v-col cols="6" class="mt-5 mb-12"> <v-col cols="6" class="mt-5 mb-12">
<p> <p>
Every time you send your 3D objects out of your application, they are captured Every time you send your 3D objects out of your application, they
as a are captured as a
<b>"commit"</b> <b>"commit"</b>
that includes a description, the sender, timestamp, and source application. that includes a description, the sender, timestamp, and source
application.
</p> </p>
</v-col> </v-col>
</v-row> </v-row>
@@ -80,8 +97,16 @@
</v-window-item> </v-window-item>
<v-window-item> <v-window-item>
<v-card class="transparent elevation-0 text-center" color="transparent" align="start"> <v-card
<v-img class="align-start mb-3" src="@/assets/onboarding-3.png" :aspect-ratio="16 / 9"> class="transparent elevation-0 text-center"
color="transparent"
align="start"
>
<v-img
class="align-start mb-3"
src="@/assets/onboarding-3.png"
:aspect-ratio="16 / 9"
>
<template #sources> <template #sources>
<source srcset="@/assets/onboarding-3.webp" /> <source srcset="@/assets/onboarding-3.webp" />
</template> </template>
@@ -111,7 +136,8 @@
<p> <p>
Create Create
<b>additional</b> <b>additional</b>
branches in your stream if you want to store parallel collections of data. branches in your stream if you want to store parallel collections
of data.
</p> </p>
<p>Use branches to keep your stream organization tidy!</p> <p>Use branches to keep your stream organization tidy!</p>
</v-col> </v-col>
@@ -122,8 +148,16 @@
</v-window-item> </v-window-item>
<v-window-item> <v-window-item>
<v-card class="transparent elevation-0 text-center" color="transparent" align="start"> <v-card
<v-img class="align-start mb-3" src="@/assets/onboarding-4.png" :aspect-ratio="16 / 9"> class="transparent elevation-0 text-center"
color="transparent"
align="start"
>
<v-img
class="align-start mb-3"
src="@/assets/onboarding-4.png"
:aspect-ratio="16 / 9"
>
<template #sources> <template #sources>
<source srcset="@/assets/onboarding-4.webp" /> <source srcset="@/assets/onboarding-4.webp" />
</template> </template>
@@ -143,8 +177,8 @@
<p> <p>
Speckle Web gives you access to all your information and Speckle Web gives you access to all your information and
<b>activity,</b> <b>activity,</b>
with a data viewer that lets you filter and customize your objects by their with a data viewer that lets you filter and customize your objects
properties. by their properties.
</p> </p>
</v-col> </v-col>
</v-row> </v-row>
@@ -154,8 +188,16 @@
</v-window-item> </v-window-item>
<v-window-item> <v-window-item>
<v-card class="transparent elevation-0 text-center" color="transparent" align="start"> <v-card
<v-img class="align-start mb-3" src="@/assets/onboarding-5.png" :aspect-ratio="16 / 9"> class="transparent elevation-0 text-center"
color="transparent"
align="start"
>
<v-img
class="align-start mb-3"
src="@/assets/onboarding-5.png"
:aspect-ratio="16 / 9"
>
<template #sources> <template #sources>
<source srcset="@/assets/onboarding-5.webp" /> <source srcset="@/assets/onboarding-5.webp" />
</template> </template>
@@ -168,8 +210,8 @@
<v-row> <v-row>
<v-col cols="5" class="mt-5 mb-12"> <v-col cols="5" class="mt-5 mb-12">
<p> <p>
Get your original 3D data back into your application with our connectors - Get your original 3D data back into your application with our
just select the commits you want to receive! connectors - just select the commits you want to receive!
</p> </p>
</v-col> </v-col>
</v-row> </v-row>
@@ -186,7 +228,9 @@
<v-window-item> <v-window-item>
<v-card class="transparent elevation-0 text-center" color="transparent"> <v-card class="transparent elevation-0 text-center" color="transparent">
<v-card-title class="display-1 justify-center my-5">🏃 Get Started!</v-card-title> <v-card-title class="display-1 justify-center my-5">
🏃 Get Started!
</v-card-title>
<v-card-subtitle class="subtitle-1 justify-center mb-5"> <v-card-subtitle class="subtitle-1 justify-center mb-5">
Time to make the most of Time to make the most of
<b>your</b> <b>your</b>
@@ -194,10 +238,12 @@
</v-card-subtitle> </v-card-subtitle>
<v-card-text class="body-1 text--primary"> <v-card-text class="body-1 text--primary">
<p> <p>
You can now start creating your own workflows for automation, interoperaility or You can now start creating your own workflows for automation,
collaboration using Speckle! interoperaility or collaboration using Speckle!
</p>
<p>
We have put together a series of tutorials that you might find useful:
</p> </p>
<p>We have put together a series of tutorials that you might find useful:</p>
</v-card-text> </v-card-text>
<v-container fluid> <v-container fluid>
<v-row dense> <v-row dense>
@@ -228,7 +274,7 @@
</v-window-item> </v-window-item>
</v-window> </v-window>
<v-card-actions class="justify-space-between pb-7"> <v-card-actions class="justify-space-between pb-7">
<v-btn text @click="prev" :disabled="onboarding == 0" rounded class="pr-4"> <v-btn text :disabled="onboarding == 0" rounded class="pr-4" @click="prev">
<v-icon>mdi-chevron-left</v-icon> <v-icon>mdi-chevron-left</v-icon>
BACK BACK
</v-btn> </v-btn>
@@ -239,7 +285,7 @@
</v-btn> </v-btn>
</v-item> </v-item>
</v-item-group> </v-item-group>
<v-btn color="primary" @click="next" rounded elevation="10" class="pl-4"> <v-btn color="primary" rounded elevation="10" class="pl-4" @click="next">
{{ onboarding == length - 1 ? 'FINISH' : 'NEXT' }} {{ onboarding == length - 1 ? 'FINISH' : 'NEXT' }}
<v-icon>mdi-chevron-right</v-icon> <v-icon>mdi-chevron-right</v-icon>
</v-btn> </v-btn>
@@ -1,7 +1,11 @@
<template> <template>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-timeline v-if="stream && groupedActivity && groupedActivity.length !== 0" align-top dense> <v-timeline
v-if="stream && groupedActivity && groupedActivity.length !== 0"
align-top
dense
>
<list-item-activity <list-item-activity
v-for="activity in groupedActivity" v-for="activity in groupedActivity"
:key="activity.time" :key="activity.time"
@@ -10,7 +14,9 @@
class="my-1" class="my-1"
></list-item-activity> ></list-item-activity>
<infinite-loading <infinite-loading
v-if="stream.activity && stream.activity.items.length < stream.activity.totalCount" v-if="
stream.activity && stream.activity.items.length < stream.activity.totalCount
"
@infinite="infiniteHandler" @infinite="infiniteHandler"
> >
<div slot="no-more">This is all your activity!</div> <div slot="no-more">This is all your activity!</div>
@@ -96,11 +102,18 @@ export default {
if (curr.actionType === test.actionType && curr.streamId === test.streamId) { if (curr.actionType === test.actionType && curr.streamId === test.streamId) {
if (curr.actionType.includes('stream_permissions')) { if (curr.actionType.includes('stream_permissions')) {
//skip multiple stream_permission actions on the same user, just pick the last! //skip multiple stream_permission actions on the same user, just pick the last!
if (prev[prev.length - 1].some((x) => x.info.targetUser === curr.info.targetUser)) if (
prev[prev.length - 1].some(
(x) => x.info.targetUser === curr.info.targetUser
)
)
action = 'skip' action = 'skip'
else action = 'combine' else action = 'combine'
} //stream, branch, commit } //stream, branch, commit
else if (curr.actionType.includes('_update') || curr.actionType === 'commit_create') else if (
curr.actionType.includes('_update') ||
curr.actionType === 'commit_create'
)
action = 'combine' action = 'combine'
} }
if (action === 'combine') { if (action === 'combine') {
@@ -33,7 +33,10 @@
</v-btn> </v-btn>
</span> </span>
<span v-else-if="!linkToCollabs && collaborators.length > 5"> <span v-else-if="!linkToCollabs && collaborators.length > 5">
<span v-tooltip="`${collaborators.length - 4} more collaborators`" class="caption"> <span
v-tooltip="`${collaborators.length - 4} more collaborators`"
class="caption"
>
+{{ collaborators.length - 4 }} +{{ collaborators.length - 4 }}
</span> </span>
</span> </span>
@@ -42,7 +42,10 @@
{{ commit.branchName }} {{ commit.branchName }}
</v-chip> </v-chip>
</span> </span>
<source-app-avatar v-if="showSourceApp" :application-name="commit.sourceApplication" /> <source-app-avatar
v-if="showSourceApp"
:application-name="commit.sourceApplication"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -54,7 +57,8 @@ export default {
components: { components: {
UserAvatar: () => import('@/main/components/common/UserAvatar'), UserAvatar: () => import('@/main/components/common/UserAvatar'),
SourceAppAvatar: () => import('@/main/components/common/SourceAppAvatar'), SourceAppAvatar: () => import('@/main/components/common/SourceAppAvatar'),
CommitReceivedReceipts: () => import('@/main/components/common/CommitReceivedReceipts') CommitReceivedReceipts: () =>
import('@/main/components/common/CommitReceivedReceipts')
}, },
props: { props: {
commit: { commit: {
@@ -150,7 +154,8 @@ export default {
}/branches/${encodeURIComponent(this.commit.branchName)}` }/branches/${encodeURIComponent(this.commit.branchName)}`
}, },
receivedUsersUnique() { receivedUsersUnique() {
if (!(this.activity && this.activity.items && this.activity.items.length > 0)) return [] if (!(this.activity && this.activity.items && this.activity.items.length > 0))
return []
let set = new Set() let set = new Set()
this.activity.items.forEach((item) => set.add(item.userId)) this.activity.items.forEach((item) => set.add(item.userId))
return Array.from(set) return Array.from(set)
@@ -14,7 +14,9 @@
<!-- <v-btn x-small color="primary">change</v-btn> --> <!-- <v-btn x-small color="primary">change</v-btn> -->
<v-menu offset-y> <v-menu offset-y>
<template #activator="{ on, attrs }"> <template #activator="{ on, attrs }">
<v-btn x-small color="" dark v-bind="attrs" :disabled="disabled" v-on="on">Change</v-btn> <v-btn x-small color="" dark v-bind="attrs" :disabled="disabled" v-on="on">
Change
</v-btn>
</template> </template>
<v-list dense> <v-list dense>
<v-list-item <v-list-item
@@ -25,7 +27,9 @@
$emit('update-user-role', userSelf) $emit('update-user-role', userSelf)
" "
> >
<v-list-item-action><v-icon small>mdi-chevron-right</v-icon></v-list-item-action> <v-list-item-action>
<v-icon small>mdi-chevron-right</v-icon>
</v-list-item-action>
<v-list-item-title>{{ item.name }}</v-list-item-title> <v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item @click="$emit('remove-user', userSelf)"> <v-list-item @click="$emit('remove-user', userSelf)">
@@ -1,7 +1,9 @@
<template> <template>
<no-data-placeholder> <no-data-placeholder>
<h2>Nothing favorited yet!</h2> <h2>Nothing favorited yet!</h2>
<p class="caption">Once you mark any streams as favorite, they're going to appear here.</p> <p class="caption">
Once you mark any streams as favorite, they're going to appear here.
</p>
<template #actions> <template #actions>
<v-list rounded class="transparent"> <v-list rounded class="transparent">
<v-list-item link class="primary mb-4" to="/streams"> <v-list-item link class="primary mb-4" to="/streams">
@@ -8,10 +8,22 @@
</template> </template>
<template slot="actions"> <template slot="actions">
<v-spacer /> <v-spacer />
<v-btn v-tooltip="'Clear all globals'" color="error" icon class="mr-2" @click="clearGlobals"> <v-btn
v-tooltip="'Clear all globals'"
color="error"
icon
class="mr-2"
@click="clearGlobals"
>
<v-icon>mdi-close</v-icon> <v-icon>mdi-close</v-icon>
</v-btn> </v-btn>
<v-btn v-tooltip="'Undo any changes'" color="primary" icon class="mr-2" @click="resetGlobals"> <v-btn
v-tooltip="'Undo any changes'"
color="primary"
icon
class="mr-2"
@click="resetGlobals"
>
<v-icon>mdi-undo</v-icon> <v-icon>mdi-undo</v-icon>
</v-btn> </v-btn>
<v-btn <v-btn
@@ -49,7 +61,12 @@
<v-progress-linear indeterminate></v-progress-linear> <v-progress-linear indeterminate></v-progress-linear>
</template> </template>
<v-card-title>Save Globals</v-card-title> <v-card-title>Save Globals</v-card-title>
<v-form ref="form" v-model="saveValid" lazy-validation @submit.prevent="saveGlobals"> <v-form
ref="form"
v-model="saveValid"
lazy-validation
@submit.prevent="saveGlobals"
>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="saveMessage" v-model="saveMessage"
@@ -66,7 +83,9 @@
</v-alert> </v-alert>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="primary" text :disabled="!saveValid" type="submit">Save</v-btn> <v-btn color="primary" text :disabled="!saveValid" type="submit">
Save
</v-btn>
</v-card-actions> </v-card-actions>
</v-form> </v-form>
</v-card> </v-card>
@@ -146,7 +165,9 @@ export default {
}, },
saveValid: false, saveValid: false,
saveLoading: false, saveLoading: false,
nameRules: [(v) => (v && v.length >= 3) || 'Message must be at least 3 characters'], nameRules: [
(v) => (v && v.length >= 3) || 'Message must be at least 3 characters'
],
saveMessage: null, saveMessage: null,
saveError: null saveError: null
} }
@@ -298,7 +319,9 @@ export default {
: this.nestedGlobals(this.sample) : this.nestedGlobals(this.sample)
}, },
clearGlobals() { clearGlobals() {
this.globalsArray = this.nestedGlobals({ placeholder: 'something cool goes here...' }) this.globalsArray = this.nestedGlobals({
placeholder: 'something cool goes here...'
})
}, },
addProp(kwargs) { addProp(kwargs) {
let globals = this.getNestedGlobals(kwargs.path) let globals = this.getNestedGlobals(kwargs.path)
@@ -24,7 +24,11 @@
dense dense
rounded rounded
/> />
<v-text-field v-model="entry.value" class="entry-value mr-5" hint="property value" /> <v-text-field
v-model="entry.value"
class="entry-value mr-5"
hint="property value"
/>
<v-btn <v-btn
v-if="true" v-if="true"
v-tooltip="'Transform this field into a nested object'" v-tooltip="'Transform this field into a nested object'"
@@ -54,10 +58,16 @@
v-if="!editTitle" v-if="!editTitle"
@mouseenter="mouseOver = true" @mouseenter="mouseOver = true"
@mouseleave="mouseOver = false" @mouseleave="mouseOver = false"
@click="editTitle=true" @click="editTitle = true"
> >
{{ entry.key }} {{ entry.key }}
<v-btn v-show="mouseOver" icon small color="primary" @click="editTitle = true"> <v-btn
v-show="mouseOver"
icon
small
color="primary"
@click="editTitle = true"
>
<v-icon small>mdi-pencil</v-icon> <v-icon small>mdi-pencil</v-icon>
</v-btn> </v-btn>
</v-toolbar-title> </v-toolbar-title>
@@ -68,9 +78,9 @@
:rules="rules.keys(index, entries)" :rules="rules.keys(index, entries)"
:error-messages="entry.valid === true ? null : entry.valid" :error-messages="entry.valid === true ? null : entry.valid"
append-icon="mdi-check" append-icon="mdi-check"
@click:append="editTitle=false" @click:append="editTitle = false"
@keyup.enter="editTitle=false" @keyup.enter="editTitle = false"
style="width: 300px; margin-top:14px;" style="width: 300px; margin-top: 14px"
></v-text-field> ></v-text-field>
</v-toolbar-title> </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@@ -175,13 +185,15 @@ export default {
(v) => { (v) => {
let filtered = entries.filter((_, i) => i != index) let filtered = entries.filter((_, i) => i != index)
let result = let result =
filtered.findIndex((e) => e.key === v) === -1 || 'Each property name must be unique' filtered.findIndex((e) => e.key === v) === -1 ||
'Each property name must be unique'
if (entries[index].valid === true) entries[index].valid = result if (entries[index].valid === true) entries[index].valid = result
return result return result
}, },
(v) => { (v) => {
const re = /[./]/ const re = /[./]/
let result = !re.test(v) || 'The name cannot contain invalid characters: "." or "/"' let result =
!re.test(v) || 'The name cannot contain invalid characters: "." or "/"'
if (entries[index].valid === true) entries[index].valid = result if (entries[index].valid === true) entries[index].valid = result
return result return result
} }
@@ -1,7 +1,12 @@
<template> <template>
<v-card class="my-4 pa-1 elevation-0" :loading="$apollo.loading"> <v-card class="my-4 pa-1 elevation-0" :loading="$apollo.loading">
<div v-if="!$apollo.loading && file" class="d-flex align-center"> <div v-if="!$apollo.loading && file" class="d-flex align-center">
<v-btn v-tooltip="`Download the original file`" icon small @click="downloadOriginalFile()"> <v-btn
v-tooltip="`Download the original file`"
icon
small
@click="downloadOriginalFile()"
>
<v-icon small>mdi-download</v-icon> <v-icon small>mdi-download</v-icon>
</v-btn> </v-btn>
@@ -12,13 +17,21 @@
<template v-if="file.convertedStatus === 0"> <template v-if="file.convertedStatus === 0">
<v-btn text small disabled> <v-btn text small disabled>
<span class="mr-2">Queued</span> <span class="mr-2">Queued</span>
<v-progress-circular indeterminate :size="20" :width="2"></v-progress-circular> <v-progress-circular
indeterminate
:size="20"
:width="2"
></v-progress-circular>
</v-btn> </v-btn>
</template> </template>
<template v-if="file.convertedStatus === 1"> <template v-if="file.convertedStatus === 1">
<v-btn text small> <v-btn text small>
<span class="mr-2">Converting</span> <span class="mr-2">Converting</span>
<v-progress-circular indeterminate :size="20" :width="2"></v-progress-circular> <v-progress-circular
indeterminate
:size="20"
:width="2"
></v-progress-circular>
</v-btn> </v-btn>
</template> </template>
<template v-if="file.convertedStatus === 2"> <template v-if="file.convertedStatus === 2">
@@ -11,7 +11,12 @@
<v-spacer /> <v-spacer />
<v-menu offset-y> <v-menu offset-y>
<template #activator="{ attrs, on }"> <template #activator="{ attrs, on }">
<v-btn v-tooltip="`Change the branch to upload to`" text v-bind="attrs" v-on="on"> <v-btn
v-tooltip="`Change the branch to upload to`"
text
v-bind="attrs"
v-on="on"
>
<v-icon small>mdi-source-branch</v-icon> <v-icon small>mdi-source-branch</v-icon>
<span class="caption">{{ selectedBranch }}</span> <span class="caption">{{ selectedBranch }}</span>
</v-btn> </v-btn>
@@ -55,7 +60,10 @@ export default {
this.selectedBranch ? this.selectedBranch : 'main' this.selectedBranch ? this.selectedBranch : 'main'
}` }`
) )
request.setRequestHeader('Authorization', `Bearer ${localStorage.getItem('AuthToken')}`) request.setRequestHeader(
'Authorization',
`Bearer ${localStorage.getItem('AuthToken')}`
)
request.upload.addEventListener( request.upload.addEventListener(
'progress', 'progress',
@@ -1,7 +1,12 @@
<template> <template>
<v-card> <v-card>
<v-card-title class="primary white--text">Edit App</v-card-title> <v-card-title class="primary white--text">Edit App</v-card-title>
<v-form v-show="!appUpdateResult" ref="form" v-model="valid" @submit.prevent="editApp"> <v-form
v-show="!appUpdateResult"
ref="form"
v-model="valid"
@submit.prevent="editApp"
>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="name" v-model="name"
@@ -53,8 +58,8 @@
></v-textarea> ></v-textarea>
<v-alert type="info" class="mt-5"> <v-alert type="info" class="mt-5">
<b>Note:</b> <b>Note:</b>
After editing an app, all users will need to authorise it again (existing tokens will be After editing an app, all users will need to authorise it again (existing
invalidated). tokens will be invalidated).
</v-alert> </v-alert>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@@ -69,7 +74,9 @@
<p> <p>
<b>Note:</b> <b>Note:</b>
To authenticate users inside your app, direct them to To authenticate users inside your app, direct them to
<code style="word-break: break-all">{{ rootUrl }}/authn/verify/{appId}/{challenge}</code> <code style="word-break: break-all">
{{ rootUrl }}/authn/verify/{appId}/{challenge}
</code>
, where , where
<code>challenge</code> <code>challenge</code>
is an OAuth2 plain code challenge. is an OAuth2 plain code challenge.
@@ -2,7 +2,12 @@
<v-card> <v-card>
<v-card-title class="primary white--text">Create a New App</v-card-title> <v-card-title class="primary white--text">Create a New App</v-card-title>
<v-form v-show="!appCreateResult" ref="form" v-model="valid" @submit.prevent="createApp"> <v-form
v-show="!appCreateResult"
ref="form"
v-model="valid"
@submit.prevent="createApp"
>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="name" v-model="name"
@@ -65,7 +70,9 @@
<p> <p>
<b>Note:</b> <b>Note:</b>
To authenticate users inside your app, direct them to To authenticate users inside your app, direct them to
<code style="word-break: break-all">{{ rootUrl }}/authn/verify/{appId}/{challenge}</code> <code style="word-break: break-all">
{{ rootUrl }}/authn/verify/{appId}/{challenge}
</code>
, where , where
<code>challenge</code> <code>challenge</code>
is an OAuth2 plain code challenge. is an OAuth2 plain code challenge.
@@ -30,7 +30,15 @@
> >
Verification email sent, please check you inbox. Verification email sent, please check you inbox.
</v-alert> </v-alert>
<v-alert v-if="errors" type="error" height="44" dismissible rounded="lg" elevation="8" dense> <v-alert
v-if="errors"
type="error"
height="44"
dismissible
rounded="lg"
elevation="8"
dense
>
Email verification failed.{{ errorMessage ? ` Reason: ${errorMessage}` : '' }} Email verification failed.{{ errorMessage ? ` Reason: ${errorMessage}` : '' }}
</v-alert> </v-alert>
</transition> </transition>
@@ -81,7 +81,7 @@ export default {
apollo: { apollo: {
appScopes: { appScopes: {
query: gql` query: gql`
query($id: String!) { query ($id: String!) {
app(id: $id) { app(id: $id) {
id id
name name
@@ -1,7 +1,12 @@
<template> <template>
<v-card class="pa-4"> <v-card class="pa-4">
<v-card-title>Create a New Personal Access Token</v-card-title> <v-card-title>Create a New Personal Access Token</v-card-title>
<v-form v-show="!fullTokenResult" ref="form" v-model="valid" @submit.prevent="createToken"> <v-form
v-show="!fullTokenResult"
ref="form"
v-model="valid"
@submit.prevent="createToken"
>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="name" v-model="name"
@@ -44,8 +49,8 @@
</div> </div>
<v-alert type="info"> <v-alert type="info">
<b>Note:</b> <b>Note:</b>
This is the first and last time you will be able to see the full token. Please copy paste it This is the first and last time you will be able to see the full token. Please
somewhere safe now. copy paste it somewhere safe now.
</v-alert> </v-alert>
<v-btn block color="primary" @click="clearAndClose">Close</v-btn> <v-btn block color="primary" @click="clearAndClose">Close</v-btn>
</v-card-text> </v-card-text>
@@ -7,11 +7,12 @@
</template> </template>
<v-card rounded="lg"> <v-card rounded="lg">
<v-card-text> <v-card-text>
Personal Access Tokens can be used to access the Speckle API on this server; they function Personal Access Tokens can be used to access the Speckle API on this server;
like ordinary OAuth access tokens. Use them in your scripts or apps! they function like ordinary OAuth access tokens. Use them in your scripts or
apps!
<b> <b>
Treat them like a password: do not post them anywhere where they could be accessed by Treat them like a password: do not post them anywhere where they could be
others (e.g., public repos). accessed by others (e.g., public repos).
</b> </b>
</v-card-text> </v-card-text>
<v-card-text v-if="$apollo.loading">Loading...</v-card-text> <v-card-text v-if="$apollo.loading">Loading...</v-card-text>
@@ -6,8 +6,8 @@
<v-btn small color="primary" @click="appDialog = true">new app</v-btn> <v-btn small color="primary" @click="appDialog = true">new app</v-btn>
</template> </template>
<v-card-text> <v-card-text>
Register and manage third-party Speckle Apps that, once authorised by a user on this server, Register and manage third-party Speckle Apps that, once authorised by a user on
can act on their behalf. this server, can act on their behalf.
</v-card-text> </v-card-text>
<v-card-text v-if="$apollo.loading">Loading...</v-card-text> <v-card-text v-if="$apollo.loading">Loading...</v-card-text>
<v-card-text v-if="apps && apps.length !== 0"> <v-card-text v-if="apps && apps.length !== 0">
@@ -1,8 +1,8 @@
<template> <template>
<v-card class="elevation-0 mt-3 mb-5 transparent"> <v-card class="elevation-0 mt-3 mb-5 transparent">
<v-card-text class=""> <v-card-text class="">
Here you can review the apps that you have granted access to. If something looks suspcious, Here you can review the apps that you have granted access to. If something looks
revoke the access! suspcious, revoke the access!
<v-btn <v-btn
v-if="!hasManager" v-if="!hasManager"
plain plain
@@ -52,12 +52,14 @@
<v-card> <v-card>
<v-card-title>Revoke Access</v-card-title> <v-card-title>Revoke Access</v-card-title>
<v-card-text> <v-card-text>
Revoking access to your app will log you out of it on all devices. Are you sure you want Revoking access to your app will log you out of it on all devices. Are you
to proceed? sure you want to proceed?
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn plain small color="error" @click="revokeAccess()">Revoke access</v-btn> <v-btn plain small color="error" @click="revokeAccess()">
Revoke access
</v-btn>
<v-btn plain small>Cancel</v-btn> <v-btn plain small>Cancel</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
@@ -93,7 +95,8 @@ export default {
} }
} }
`, `,
update: (data) => data.user.authorizedApps.filter((app) => app.id !== 'spklwebapp') update: (data) =>
data.user.authorizedApps.filter((app) => app.id !== 'spklwebapp')
} }
}, },
computed: { computed: {
@@ -8,12 +8,14 @@
<v-toolbar flat class="error--text" dense> <v-toolbar flat class="error--text" dense>
<v-toolbar-title>Delete Account</v-toolbar-title> <v-toolbar-title>Delete Account</v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon @click="showDelete = !showDelete"><v-icon>mdi-chevron-down</v-icon></v-btn> <v-btn icon @click="showDelete = !showDelete">
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
</v-toolbar> </v-toolbar>
<div v-show="showDelete"> <div v-show="showDelete">
<v-card-text> <v-card-text>
This action cannot be undone. We will delete all streams where you are the sole owner, This action cannot be undone. We will delete all streams where you are the
and any associated data. sole owner, and any associated data.
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-btn block @click="deleteUser">Delete account</v-btn> <v-btn block @click="deleteUser">Delete account</v-btn>
@@ -9,10 +9,20 @@
&nbsp; &nbsp;
<b>{{ user.name }}</b> <b>{{ user.name }}</b>
<span v-if="isSelf">!</span> <span v-if="isSelf">!</span>
<v-icon v-if="user.verified" v-tooltip="'Verfied email'" small class="ml-3 primary--text"> <v-icon
v-if="user.verified"
v-tooltip="'Verfied email'"
small
class="ml-3 primary--text"
>
mdi-shield-check mdi-shield-check
</v-icon> </v-icon>
<v-icon v-else v-tooltip="'Email not verified'" small class="mr-2 warning--text"> <v-icon
v-else
v-tooltip="'Email not verified'"
small
class="mr-2 warning--text"
>
mdi-shield-alert mdi-shield-alert
</v-icon> </v-icon>
</template> </template>
@@ -46,7 +56,9 @@
<v-icon color="red darken-3">mdi-heart</v-icon> <v-icon color="red darken-3">mdi-heart</v-icon>
</span> </span>
</template> </template>
<span>Total amount of favorites for all streams owned by this user</span> <span>
Total amount of favorites for all streams owned by this user
</span>
</v-tooltip> </v-tooltip>
</p> </p>
<span v-if="isSelf" class="caption"> <span v-if="isSelf" class="caption">
@@ -29,7 +29,9 @@
<v-icon <v-icon
class="primary--text" class="primary--text"
large large
:style="`opacity: ${user.hidden ? '0.2' : 1}; position: relative; right: -60%; font-size: 4.2em`" :style="`opacity: ${
user.hidden ? '0.2' : 1
}; position: relative; right: -60%; font-size: 4.2em`"
> >
mdi-menu-right mdi-menu-right
</v-icon> </v-icon>
@@ -38,10 +40,14 @@
v-for="sessionUser in users" v-for="sessionUser in users"
:ref="`user-bubble-${sessionUser.uuid}`" :ref="`user-bubble-${sessionUser.uuid}`"
:key="sessionUser.uuid" :key="sessionUser.uuid"
:class="`${sessionUser.name === 'Anonymous Viewer' ? 'background' : '' } absolute-pos rounded-pill user-bubble elevation-5`" :class="`${
:style="`opacity: ${sessionUser.hidden ? '0.2' : 1}; border: 4px solid #047EFB;`" sessionUser.name === 'Anonymous Viewer' ? 'background' : ''
} absolute-pos rounded-pill user-bubble elevation-5`"
:style="`opacity: ${
sessionUser.hidden ? '0.2' : 1
}; border: 4px solid #047EFB;`"
> >
<div @click="setUserPow(sessionUser)" > <div @click="setUserPow(sessionUser)">
<user-avatar <user-avatar
v-if="sessionUser.name !== 'Anonymous Viewer'" v-if="sessionUser.name !== 'Anonymous Viewer'"
:id="sessionUser.id" :id="sessionUser.id"
@@ -50,7 +56,13 @@
:size="30" :size="30"
:margin="false" :margin="false"
></user-avatar> ></user-avatar>
<v-avatar color="background" :size="30" v-else v-tooltip="sessionUser.name" style="cursor: pointer;"> <v-avatar
color="background"
:size="30"
v-else
v-tooltip="sessionUser.name"
style="cursor: pointer"
>
👀 👀
</v-avatar> </v-avatar>
<text-dots-typing v-if="sessionUser.status === 'writing'" /> <text-dots-typing v-if="sessionUser.status === 'writing'" />
@@ -101,7 +113,7 @@ export default {
$subscribe: { $subscribe: {
userViewerActivity: { userViewerActivity: {
query: gql` query: gql`
subscription($streamId: String!, $resourceId: String!) { subscription ($streamId: String!, $resourceId: String!) {
userViewerActivity(streamId: $streamId, resourceId: $resourceId) userViewerActivity(streamId: $streamId, resourceId: $resourceId)
} }
`, `,
@@ -114,18 +126,25 @@ export default {
skip() { skip() {
return !this.$route.params.resourceId || !this.$loggedIn() return !this.$route.params.resourceId || !this.$loggedIn()
}, },
result( res ) { result(res) {
let data = res.data let data = res.data
// Note: swap user id checks for .userId (vs. uuid) if wanting to not allow same user two diff browsers // Note: swap user id checks for .userId (vs. uuid) if wanting to not allow same user two diff browsers
// it's easier to test like this though :) // it's easier to test like this though :)
if(!data.userViewerActivity) return if (!data.userViewerActivity) return
if (data.userViewerActivity.status && data.userViewerActivity.status === 'disconnect') { if (
this.users = this.users.filter((u) => u.uuid !== data.userViewerActivity.uuid) data.userViewerActivity.status &&
data.userViewerActivity.status === 'disconnect'
) {
this.users = this.users.filter(
(u) => u.uuid !== data.userViewerActivity.uuid
)
this.updateBubbles(true) this.updateBubbles(true)
return return
} }
if (data.userViewerActivity.uuid === this.uuid) return if (data.userViewerActivity.uuid === this.uuid) return
let indx = this.users.findIndex((u) => u.uuid === data.userViewerActivity.uuid) let indx = this.users.findIndex(
(u) => u.uuid === data.userViewerActivity.uuid
)
if (indx !== -1) { if (indx !== -1) {
let user = this.users[indx] let user = this.users[indx]
user.hidden = false user.hidden = false
@@ -284,7 +303,11 @@ export default {
$resourceId: String! $resourceId: String!
$data: JSONObject $data: JSONObject
) { ) {
userViewerActivityBroadcast(streamId: $streamId, resourceId: $resourceId, data: $data) userViewerActivityBroadcast(
streamId: $streamId
resourceId: $resourceId
data: $data
)
} }
`, `,
variables: { variables: {
@@ -303,7 +326,11 @@ export default {
$resourceId: String! $resourceId: String!
$data: JSONObject $data: JSONObject
) { ) {
userViewerActivityBroadcast(streamId: $streamId, resourceId: $resourceId, data: $data) userViewerActivityBroadcast(
streamId: $streamId
resourceId: $resourceId
data: $data
)
} }
`, `,
variables: { variables: {
@@ -402,7 +429,10 @@ export default {
uTargetEl.style.transform = `translate(-50%, -50%) translate(${targetLoc.x}px,${targetLoc.y}px)` uTargetEl.style.transform = `translate(-50%, -50%) translate(${targetLoc.x}px,${targetLoc.y}px)`
uTargetEl.style.opacity = user.clipped ? '0' : '1' uTargetEl.style.opacity = user.clipped ? '0' : '1'
const angle = Math.atan2(targetLoc.y - 16 - newTarget.y, targetLoc.x - 16 - newTarget.x) const angle = Math.atan2(
targetLoc.y - 16 - newTarget.y,
targetLoc.x - 16 - newTarget.x
)
uArrowEl.style.transform = `translate(${newTarget.x}px,${newTarget.y}px) rotate(${angle}rad)` uArrowEl.style.transform = `translate(${newTarget.x}px,${newTarget.y}px) rotate(${angle}rad)`
uArrowEl.style.opacity = user.clipped ? '0' : '1' uArrowEl.style.opacity = user.clipped ? '0' : '1'
} }
@@ -15,7 +15,11 @@
</v-btn> </v-btn>
</template> </template>
<v-list dense nav> <v-list dense nav>
<v-list-item v-for="(item, index) in items" :key="index" @click="setView(item.title)"> <v-list-item
v-for="(item, index) in items"
:key="index"
@click="setView(item.title)"
>
<v-list-item-title> <v-list-item-title>
<v-icon small>mdi-camera-control</v-icon> <v-icon small>mdi-camera-control</v-icon>
{{ item.title }} {{ item.title }}
@@ -33,11 +33,15 @@
:class="`mouse elevation-5 ${!expand ? 'primary' : 'background'} mr-2`" :class="`mouse elevation-5 ${!expand ? 'primary' : 'background'} mr-2`"
@click="toggleExpand()" @click="toggleExpand()"
> >
<v-icon v-if="!expand" dark >mdi-plus</v-icon> <v-icon v-if="!expand" dark>mdi-plus</v-icon>
<v-icon v-else dark x-small>mdi-close</v-icon> <v-icon v-else dark x-small>mdi-close</v-icon>
</v-btn> </v-btn>
<v-slide-x-transition> <v-slide-x-transition>
<div v-if="expand && !$vuetify.breakpoint.xs" style="width: 100%" class="d-flex"> <div
v-if="expand && !$vuetify.breakpoint.xs"
style="width: 100%"
class="d-flex"
>
<v-textarea <v-textarea
v-if="$loggedIn()" v-if="$loggedIn()"
v-model="commentText" v-model="commentText"
@@ -160,7 +164,10 @@ export default {
}, },
mounted() { mounted() {
window.__viewer.on('select', debounce(this.handleSelect, 10)) window.__viewer.on('select', debounce(this.handleSelect, 10))
window.__viewer.cameraHandler.controls.addEventListener('update', this.updateCommentBubble) window.__viewer.cameraHandler.controls.addEventListener(
'update',
this.updateCommentBubble
)
// this.$refs.commentTextArea.calculateInputHeight() // this.$refs.commentTextArea.calculateInputHeight()
document.addEventListener( document.addEventListener(
'keyup', 'keyup',
@@ -258,8 +265,16 @@ export default {
if (!this.$refs.commentButton) return if (!this.$refs.commentButton) return
this.visible = true this.visible = true
let projectedLocation = new THREE.Vector3(info.location.x, info.location.y, info.location.z) let projectedLocation = new THREE.Vector3(
this.location = new THREE.Vector3(info.location.x, info.location.y, info.location.z) info.location.x,
info.location.y,
info.location.z
)
this.location = new THREE.Vector3(
info.location.x,
info.location.y,
info.location.z
)
let cam = window.__viewer.cameraHandler.camera let cam = window.__viewer.cameraHandler.camera
cam.updateProjectionMatrix() cam.updateProjectionMatrix()
@@ -267,8 +282,10 @@ export default {
let collapsedSize = this.$refs.commentButton.clientWidth let collapsedSize = this.$refs.commentButton.clientWidth
collapsedSize = 36 collapsedSize = 36
const mappedLocation = new THREE.Vector3( const mappedLocation = new THREE.Vector3(
(projectedLocation.x * 0.5 + 0.5) * this.$refs.parent.clientWidth - collapsedSize / 2, (projectedLocation.x * 0.5 + 0.5) * this.$refs.parent.clientWidth -
(projectedLocation.y * -0.5 + 0.5) * this.$refs.parent.clientHeight - collapsedSize / 1, collapsedSize / 2,
(projectedLocation.y * -0.5 + 0.5) * this.$refs.parent.clientHeight -
collapsedSize / 1,
0 0
) )
this.$refs.commentButton.style.transform = '' this.$refs.commentButton.style.transform = ''
@@ -287,8 +304,10 @@ export default {
let collapsedSize = this.$refs.commentButton.clientWidth let collapsedSize = this.$refs.commentButton.clientWidth
collapsedSize = 36 collapsedSize = 36
const mappedLocation = new THREE.Vector3( const mappedLocation = new THREE.Vector3(
(projectedLocation.x * 0.5 + 0.5) * this.$refs.parent.clientWidth - collapsedSize / 2, (projectedLocation.x * 0.5 + 0.5) * this.$refs.parent.clientWidth -
(projectedLocation.y * -0.5 + 0.5) * this.$refs.parent.clientHeight - collapsedSize / 1, collapsedSize / 2,
(projectedLocation.y * -0.5 + 0.5) * this.$refs.parent.clientHeight -
collapsedSize / 1,
0 0
) )
this.$refs.commentButton.style.transform = '' this.$refs.commentButton.style.transform = ''
@@ -4,12 +4,24 @@
--> -->
<div <div
ref="parent" ref="parent"
style="width: 100%; height: 100vh; position: absolute; pointer-events: none; overflow: hidden" style="
width: 100%;
height: 100vh;
position: absolute;
pointer-events: none;
overflow: hidden;
"
class="d-flex align-center justify-center no-mouse" class="d-flex align-center justify-center no-mouse"
> >
<div <div
v-show="showComments && !$store.state.addingComment" v-show="showComments && !$store.state.addingComment"
style="width: 100%; height: 100vh; position: absolute; pointer-events: none; overflow: hidden" style="
width: 100%;
height: 100vh;
position: absolute;
pointer-events: none;
overflow: hidden;
"
class="no-mouse" class="no-mouse"
> >
<!-- Comment bubbles --> <!-- Comment bubbles -->
@@ -18,8 +30,13 @@
:key="comment.id" :key="comment.id"
:ref="`comment-${comment.id}`" :ref="`comment-${comment.id}`"
:class="`absolute-pos rounded-xl no-mouse`" :class="`absolute-pos rounded-xl no-mouse`"
:style="`transition: opacity 0.2s ease; z-index:${comment.expanded ? '20' : '10'}; ${ :style="`transition: opacity 0.2s ease; z-index:${
hasExpandedComment && !comment.expanded && !comment.hovered && !comment.bouncing comment.expanded ? '20' : '10'
}; ${
hasExpandedComment &&
!comment.expanded &&
!comment.hovered &&
!comment.bouncing
? 'opacity: 0.1;' ? 'opacity: 0.1;'
: 'opacity: 1;' : 'opacity: 1;'
}`" }`"
@@ -38,7 +55,9 @@
? 'dark white--text primary' ? 'dark white--text primary'
: 'background' : 'background'
}`" }`"
@click="comment.expanded ? collapseComment(comment) : expandComment(comment)" @click="
comment.expanded ? collapseComment(comment) : expandComment(comment)
"
> >
<v-icon v-if="!comment.expanded" x-small class="">mdi-comment</v-icon> <v-icon v-if="!comment.expanded" x-small class="">mdi-comment</v-icon>
<v-icon v-if="comment.expanded" x-small class="">mdi-close</v-icon> <v-icon v-if="comment.expanded" x-small class="">mdi-close</v-icon>
@@ -49,7 +68,10 @@
style="position: absolute; left: 30px; width: max-content" style="position: absolute; left: 30px; width: max-content"
class="rounded-xl primary white--text px-2 ml-1 caption" class="rounded-xl primary white--text px-2 ml-1 caption"
> >
<timeago :datetime="comment.updatedAt" class="font-italic mr-2"></timeago> <timeago
:datetime="comment.updatedAt"
class="font-italic mr-2"
></timeago>
<v-icon x-small class="white--text">mdi-comment-outline</v-icon> <v-icon x-small class="white--text">mdi-comment-outline</v-icon>
{{ comment.replies.totalCount + 1 }} {{ comment.replies.totalCount + 1 }}
<v-icon v-if="comment.data.filters" x-small class="white--text"> <v-icon v-if="comment.data.filters" x-small class="white--text">
@@ -124,12 +146,13 @@ import gql from 'graphql-tag'
export default { export default {
components: { components: {
CommentThreadViewer: () => import('@/main/components/comments/CommentThreadViewer'), CommentThreadViewer: () => import('@/main/components/comments/CommentThreadViewer'),
CommentsViewerNavbar: () => import('@/main/components/comments/CommentsViewerNavbar') CommentsViewerNavbar: () =>
import('@/main/components/comments/CommentsViewerNavbar')
}, },
apollo: { apollo: {
comments: { comments: {
query: gql` query: gql`
query($streamId: String!, $resources: [ResourceIdentifierInput]!) { query ($streamId: String!, $resources: [ResourceIdentifierInput]!) {
comments(streamId: $streamId, resources: $resources, limit: 1000) { comments(streamId: $streamId, resources: $resources, limit: 1000) {
totalCount totalCount
cursor cursor
@@ -142,7 +165,7 @@ export default {
viewedAt viewedAt
archived archived
data data
resources{ resources {
resourceId resourceId
resourceType resourceType
} }
@@ -176,33 +199,42 @@ export default {
} }
}, },
result({ data }) { result({ data }) {
if(!data) return if (!data) return
for (let c of data.comments.items) { for (let c of data.comments.items) {
c.expanded = false c.expanded = false
c.hovered = false c.hovered = false
c.bouncing = false c.bouncing = false
if (this.localComments.findIndex((lc) => c.id === lc.id) === -1 && !c.archived) { if (
this.localComments.findIndex((lc) => c.id === lc.id) === -1 &&
!c.archived
) {
this.localComments.push({ ...c }) this.localComments.push({ ...c })
} }
} }
return data return data
}, },
subscribeToMore:{ subscribeToMore: {
document: gql` document: gql`
subscription($streamId: String!, $resourceIds: [String]) { subscription ($streamId: String!, $resourceIds: [String]) {
commentActivity(streamId: $streamId, resourceIds: $resourceIds) commentActivity(streamId: $streamId, resourceIds: $resourceIds)
} }
`, `,
variables() { variables() {
let resIds = [this.$route.params.resourceId] let resIds = [this.$route.params.resourceId]
if(this.$route.query.overlay) resIds = [...resIds, ...this.$route.query.overlay.split(',')] if (this.$route.query.overlay)
resIds = [...resIds, ...this.$route.query.overlay.split(',')]
return { return {
streamId: this.$route.params.streamId, streamId: this.$route.params.streamId,
resourceIds: resIds resourceIds: resIds
} }
}, },
updateQuery(prevResult, {subscriptionData}) { updateQuery(prevResult, { subscriptionData }) {
if(!subscriptionData || !subscriptionData.data || !subscriptionData.data.commentActivity) return if (
!subscriptionData ||
!subscriptionData.data ||
!subscriptionData.data.commentActivity
)
return
let newComment = subscriptionData.data.commentActivity let newComment = subscriptionData.data.commentActivity
newComment.expanded = false newComment.expanded = false
@@ -214,12 +246,11 @@ export default {
newComment.archived = false newComment.archived = false
if(subscriptionData.data.commentActivity.eventType === 'comment-added') { if (subscriptionData.data.commentActivity.eventType === 'comment-added') {
if(prevResult.comments.items.find( c => c.id === newComment.id)) { if (prevResult.comments.items.find((c) => c.id === newComment.id)) {
return return
} }
if(!newComment.archived) if (!newComment.archived) this.localComments.push(newComment)
this.localComments.push(newComment)
setTimeout(() => { setTimeout(() => {
this.updateCommentBubbles() this.updateCommentBubbles()
@@ -239,7 +270,7 @@ export default {
}, },
computed: { computed: {
activeComments() { activeComments() {
return this.localComments.filter(c => !c.archived) return this.localComments.filter((c) => !c.archived)
}, },
hasExpandedComment() { hasExpandedComment() {
return this.localComments.filter((c) => c.expanded).length !== 0 return this.localComments.filter((c) => c.expanded).length !== 0
@@ -282,7 +313,7 @@ export default {
window.__viewer.cameraHandler.controls.addEventListener('update', () => window.__viewer.cameraHandler.controls.addEventListener('update', () =>
this.updateCommentBubbles() this.updateCommentBubbles()
) )
setTimeout(()=>{ setTimeout(() => {
this.updateCommentBubbles() this.updateCommentBubbles()
}, 1000) }, 1000)
}, },
@@ -350,7 +381,7 @@ export default {
}, },
async handleDeletion(comment) { async handleDeletion(comment) {
this.collapseComment(comment) this.collapseComment(comment)
let comm = this.localComments.find(c => c.id === comment.id) let comm = this.localComments.find((c) => c.id === comment.id)
comm.archived = true comm.archived = true
this.updateCommentBubbles() this.updateCommentBubbles()
}, },
@@ -387,14 +418,16 @@ export default {
const paddingYBottom = 90 const paddingYBottom = 90
if (tX < -300) if (tX < -300)
if (!comment.preventAutoClose && !this.$vuetify.breakpoint.xs) comment.expanded = false // collapse if too far out leftwise if (!comment.preventAutoClose && !this.$vuetify.breakpoint.xs)
comment.expanded = false // collapse if too far out leftwise
if (tX < paddingX) { if (tX < paddingX) {
tX = paddingX tX = paddingX
} }
if (tX > this.$refs.parent.clientWidth - (paddingX + 50)) { if (tX > this.$refs.parent.clientWidth - (paddingX + 50)) {
tX = this.$refs.parent.clientWidth - (paddingX + 50) tX = this.$refs.parent.clientWidth - (paddingX + 50)
if (!comment.preventAutoClose && !this.$vuetify.breakpoint.xs) comment.expanded = false // collapse if too far down right if (!comment.preventAutoClose && !this.$vuetify.breakpoint.xs)
comment.expanded = false // collapse if too far down right
} }
if (tY < 0 && !comment.preventAutoClose && !this.$vuetify.breakpoint.xs) if (tY < 0 && !comment.preventAutoClose && !this.$vuetify.breakpoint.xs)
comment.expanded = false // collapse if too far out topwise comment.expanded = false // collapse if too far out topwise
@@ -26,13 +26,20 @@
> >
<v-icon x-small>mdi-close</v-icon> <v-icon x-small>mdi-close</v-icon>
</v-btn> </v-btn>
<v-btn v-tooltip="'Toggle visibility'" small icon @click.stop="toggleVisibility()"> <v-btn
v-tooltip="'Toggle visibility'"
small
icon
@click.stop="toggleVisibility()"
>
<v-icon class="grey--text" style="font-size: 12px"> <v-icon class="grey--text" style="font-size: 12px">
{{ visible ? 'mdi-eye' : 'mdi-eye-off' }} {{ visible ? 'mdi-eye' : 'mdi-eye-off' }}
</v-icon> </v-icon>
</v-btn> </v-btn>
<v-btn v-tooltip="'Isolate objects'" small icon @click.stop="isolate()"> <v-btn v-tooltip="'Isolate objects'" small icon @click.stop="isolate()">
<v-icon x-small :class="`${isolated ? 'primary--text' : ''}`">mdi-filter</v-icon> <v-icon x-small :class="`${isolated ? 'primary--text' : ''}`">
mdi-filter
</v-icon>
</v-btn> </v-btn>
<v-btn small icon @click.stop="expanded = !expanded"> <v-btn small icon @click.stop="expanded = !expanded">
<v-icon x-small>{{ expanded ? 'mdi-minus' : 'mdi-plus' }}</v-icon> <v-icon x-small>{{ expanded ? 'mdi-minus' : 'mdi-plus' }}</v-icon>
@@ -81,25 +88,45 @@ export default {
}, },
isolated() { isolated() {
return ( return (
this.$store.state.isolateValues.indexOf(this.resource.data.commit.referencedObject) !== -1 this.$store.state.isolateValues.indexOf(
this.resource.data.commit.referencedObject
) !== -1
) )
}, },
visible() { visible() {
return this.$store.state.hideValues.indexOf(this.resource.data.commit.referencedObject) === -1 return (
this.$store.state.hideValues.indexOf(
this.resource.data.commit.referencedObject
) === -1
)
} }
}, },
methods: { methods: {
isolate() { isolate() {
let id = this.resource.data.commit.referencedObject let id = this.resource.data.commit.referencedObject
if (this.isolated) if (this.isolated)
this.$store.commit('unisolateObjects', { filterKey: '__parents', filterValues: [id] }) this.$store.commit('unisolateObjects', {
else this.$store.commit('isolateObjects', { filterKey: '__parents', filterValues: [id] }) filterKey: '__parents',
filterValues: [id]
})
else
this.$store.commit('isolateObjects', {
filterKey: '__parents',
filterValues: [id]
})
}, },
toggleVisibility() { toggleVisibility() {
let id = this.resource.data.commit.referencedObject let id = this.resource.data.commit.referencedObject
if (this.visible) if (this.visible)
this.$store.commit('hideObjects', { filterKey: '__parents', filterValues: [id] }) this.$store.commit('hideObjects', {
else this.$store.commit('showObjects', { filterKey: '__parents', filterValues: [id] }) filterKey: '__parents',
filterValues: [id]
})
else
this.$store.commit('showObjects', {
filterKey: '__parents',
filterValues: [id]
})
} }
} }
} }
@@ -8,7 +8,9 @@
icon icon
@click.stop="toggleColors()" @click.stop="toggleColors()"
> >
<v-icon small :class="`${colorBy ? 'primary--text' : ''}`">mdi-palette</v-icon> <v-icon small :class="`${colorBy ? 'primary--text' : ''}`">
mdi-palette
</v-icon>
</v-btn> </v-btn>
</v-list-item-action> </v-list-item-action>
</portal> </portal>
@@ -20,7 +22,9 @@
> >
<v-col <v-col
cols="1" cols="1"
:class="`text-center text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`" :class="`text-center text-truncate px-1 ${
$vuetify.theme.dark ? 'grey--text' : ''
}`"
style="line-height: 24px; font-size: 9px" style="line-height: 24px; font-size: 9px"
> >
{{ type.count }} {{ type.count }}
@@ -35,13 +39,17 @@
</v-col> </v-col>
<v-col <v-col
cols="4" cols="4"
:class="`caption text-truncate text-right px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`" :class="`caption text-truncate text-right px-1 ${
$vuetify.theme.dark ? 'grey--text' : ''
}`"
style="line-height: 24px" style="line-height: 24px"
> >
<div <div
v-if="colorBy" v-if="colorBy"
class="d-inline-block rounded mr-3 mt-1 elevation-3" class="d-inline-block rounded mr-3 mt-1 elevation-3"
:style="`width: 8px; height: 8px; background:${$store.state.colorLegend[type.fullName]};`" :style="`width: 8px; height: 8px; background:${
$store.state.colorLegend[type.fullName]
};`"
></div> ></div>
<v-btn <v-btn
v-tooltip="'Toggle visibility'" v-tooltip="'Toggle visibility'"
@@ -8,23 +8,33 @@
icon icon
@click.stop="colorBy = !colorBy" @click.stop="colorBy = !colorBy"
> >
<v-icon small :class="`${colorBy ? 'primary--text' : ''}`">mdi-palette</v-icon> <v-icon small :class="`${colorBy ? 'primary--text' : ''}`">
mdi-palette
</v-icon>
</v-btn> </v-btn>
</v-list-item-action> </v-list-item-action>
</portal> </portal>
<v-row no-gutters class="my-1 property-row rounded-lg"> <v-row no-gutters class="my-1 property-row rounded-lg">
<v-col <v-col
cols="1" cols="1"
:class="`text-center text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`" :class="`text-center text-truncate px-1 ${
$vuetify.theme.dark ? 'grey--text' : ''
}`"
style="line-height: 24px; font-size: 9px" style="line-height: 24px; font-size: 9px"
> >
<v-icon small style="font-size: 12px" :class="`${$vuetify.theme.dark ? 'grey--text' : ''}`"> <v-icon
small
style="font-size: 12px"
:class="`${$vuetify.theme.dark ? 'grey--text' : ''}`"
>
mdi-information-outline mdi-information-outline
</v-icon> </v-icon>
</v-col> </v-col>
<v-col <v-col
cols="11" cols="11"
:class="`caption text-truncatexxx px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`" :class="`caption text-truncatexxx px-1 ${
$vuetify.theme.dark ? 'grey--text' : ''
}`"
style="line-height: 24px" style="line-height: 24px"
> >
{{ filter.data.objectCount }} elements; min: {{ filter.data.objectCount }} elements; min:
@@ -34,7 +44,9 @@
<v-col <v-col
v-if="filter.data.maxValue === filter.data.minValue" v-if="filter.data.maxValue === filter.data.minValue"
cols="12" cols="12"
:class="`caption text-truncatexxx px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`" :class="`caption text-truncatexxx px-1 ${
$vuetify.theme.dark ? 'grey--text' : ''
}`"
style="line-height: 24px" style="line-height: 24px"
> >
Invalid values (min value equals to max value). Invalid values (min value equals to max value).
@@ -144,6 +156,10 @@ export default {
} }
.super-slider .v-slider__track-fill { .super-slider .v-slider__track-fill {
background: linear-gradient(to left, #fc466b, #3f5efb) !important; background: linear-gradient(to left, #fc466b, #3f5efb) !important;
background: -webkit-linear-gradient(to left, #fc466b, #3f5efb); /* Chrome 10-25, Safari 5.1-6 */ background: -webkit-linear-gradient(
to left,
#fc466b,
#3f5efb
); /* Chrome 10-25, Safari 5.1-6 */
} }
</style> </style>
@@ -1,7 +1,15 @@
<template> <template>
<v-row no-gutters class="my-1 property-row rounded-lg" @click="$emit('active-toggle', filter)"> <v-row
no-gutters
class="my-1 property-row rounded-lg"
@click="$emit('active-toggle', filter)"
>
<v-col cols="1" class="text-center" style="line-height: 30px"> <v-col cols="1" class="text-center" style="line-height: 30px">
<v-icon small style="font-size: 12px" :class="`${$vuetify.theme.dark ? 'grey--text' : ''}`"> <v-icon
small
style="font-size: 12px"
:class="`${$vuetify.theme.dark ? 'grey--text' : ''}`"
>
{{ filter.data.type === 'number' ? 'mdi-numeric' : 'mdi-format-text' }} {{ filter.data.type === 'number' ? 'mdi-numeric' : 'mdi-format-text' }}
</v-icon> </v-icon>
</v-col> </v-col>
@@ -43,7 +43,10 @@
<filter-category-active :filter="activeFilter" /> <filter-category-active :filter="activeFilter" />
</div> </div>
<div v-if="activeFilter && activeFilter.data.type === 'number'"> <div v-if="activeFilter && activeFilter.data.type === 'number'">
<filter-numeric-active :filter="activeFilter" :prevent-first-set="preventFirstSet" /> <filter-numeric-active
:filter="activeFilter"
:prevent-first-set="preventFirstSet"
/>
</div> </div>
<div v-show="activeFilter === null"> <div v-show="activeFilter === null">
<div class=""> <div class="">
@@ -67,9 +70,14 @@
/> />
</div> </div>
</div> </div>
<v-subheader>{{ filterSearch ? 'Matching' : 'Other' }} filters:</v-subheader> <v-subheader>
{{ filterSearch ? 'Matching' : 'Other' }} filters:
</v-subheader>
<div v-for="filter in matchingFilters" :key="filter.targetKey"> <div v-for="filter in matchingFilters" :key="filter.targetKey">
<filter-row-select :filter="filter" @active-toggle="(e) => (activeFilter = e)" /> <filter-row-select
:filter="filter"
@active-toggle="(e) => (activeFilter = e)"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -22,13 +22,20 @@
> >
<v-icon x-small>mdi-close</v-icon> <v-icon x-small>mdi-close</v-icon>
</v-btn> </v-btn>
<v-btn v-tooltip="'Toggle visibility'" small icon @click.stop="toggleVisibility()"> <v-btn
v-tooltip="'Toggle visibility'"
small
icon
@click.stop="toggleVisibility()"
>
<v-icon class="grey--text" style="font-size: 12px"> <v-icon class="grey--text" style="font-size: 12px">
{{ visible ? 'mdi-eye' : 'mdi-eye-off' }} {{ visible ? 'mdi-eye' : 'mdi-eye-off' }}
</v-icon> </v-icon>
</v-btn> </v-btn>
<v-btn v-tooltip="'Isolate objects'" small icon @click.stop="isolate()"> <v-btn v-tooltip="'Isolate objects'" small icon @click.stop="isolate()">
<v-icon x-small :class="`${isolated ? 'primary--text' : ''}`">mdi-filter</v-icon> <v-icon x-small :class="`${isolated ? 'primary--text' : ''}`">
mdi-filter
</v-icon>
</v-btn> </v-btn>
<v-btn small icon @click.stop="expanded = !expanded"> <v-btn small icon @click.stop="expanded = !expanded">
<v-icon x-small>{{ expanded ? 'mdi-minus' : 'mdi-plus' }}</v-icon> <v-icon x-small>{{ expanded ? 'mdi-minus' : 'mdi-plus' }}</v-icon>
@@ -60,7 +67,9 @@ export default {
}, },
computed: { computed: {
isolated() { isolated() {
return this.$store.state.isolateValues.indexOf(this.resource.data.object.id) !== -1 return (
this.$store.state.isolateValues.indexOf(this.resource.data.object.id) !== -1
)
}, },
visible() { visible() {
return this.$store.state.hideValues.indexOf(this.resource.data.object.id) === -1 return this.$store.state.hideValues.indexOf(this.resource.data.object.id) === -1
@@ -70,14 +79,28 @@ export default {
isolate() { isolate() {
let id = this.resource.data.object.id let id = this.resource.data.object.id
if (this.isolated) if (this.isolated)
this.$store.commit('unisolateObjects', { filterKey: '__parents', filterValues: [id] }) this.$store.commit('unisolateObjects', {
else this.$store.commit('isolateObjects', { filterKey: '__parents', filterValues: [id] }) filterKey: '__parents',
filterValues: [id]
})
else
this.$store.commit('isolateObjects', {
filterKey: '__parents',
filterValues: [id]
})
}, },
toggleVisibility() { toggleVisibility() {
let id = this.resource.data.object.id let id = this.resource.data.object.id
if (this.visible) if (this.visible)
this.$store.commit('hideObjects', { filterKey: '__parents', filterValues: [id] }) this.$store.commit('hideObjects', {
else this.$store.commit('showObjects', { filterKey: '__parents', filterValues: [id] }) filterKey: '__parents',
filterValues: [id]
})
else
this.$store.commit('showObjects', {
filterKey: '__parents',
filterValues: [id]
})
} }
} }
} }
@@ -98,7 +98,9 @@ export default {
for (let key of Object.keys(this.realObject)) { for (let key of Object.keys(this.realObject)) {
if (this.ignoredProps.indexOf(key) !== -1) continue if (this.ignoredProps.indexOf(key) !== -1) continue
let value = this.realObject[key] let value = this.realObject[key]
let type = Array.isArray(this.realObject[key]) ? 'array' : typeof this.realObject[key] let type = Array.isArray(this.realObject[key])
? 'array'
: typeof this.realObject[key]
let extras = [] let extras = []
if (value?.referencedId) extras.push('open', 'visibility') if (value?.referencedId) extras.push('open', 'visibility')
if ( if (
@@ -1,17 +1,29 @@
<template> <template>
<v-row <v-row
no-gutters no-gutters
:class="`my-1 py-1 property-row rounded-lg ${$vuetify.theme.dark ? 'black-bg' : 'white-bg'} ${ :class="`my-1 py-1 property-row rounded-lg ${
prop.type === 'object' || prop.type === 'array' ? (expanded ? 'border-blue' : 'border') : '' $vuetify.theme.dark ? 'black-bg' : 'white-bg'
} ${
prop.type === 'object' || prop.type === 'array'
? expanded
? 'border-blue'
: 'border'
: ''
} ${ } ${
prop.type === 'object' || prop.type === 'array' prop.type === 'object' || prop.type === 'array'
? 'hover-cursor property-row-hover' ? 'hover-cursor property-row-hover'
: 'normal-cursor' : 'normal-cursor'
}`" }`"
@click.stop="prop.type === 'object' || prop.type === 'array' ? (expanded = !expanded) : null" @click.stop="
prop.type === 'object' || prop.type === 'array' ? (expanded = !expanded) : null
"
> >
<v-col cols="1" class="text-center"> <v-col cols="1" class="text-center">
<v-icon small style="font-size: 12px" :class="`${$vuetify.theme.dark ? 'grey--text' : ''}`"> <v-icon
small
style="font-size: 12px"
:class="`${$vuetify.theme.dark ? 'grey--text' : ''}`"
>
{{ icon }} {{ icon }}
</v-icon> </v-icon>
</v-col> </v-col>
@@ -79,18 +91,32 @@
class="mr-1" class="mr-1"
@click.stop="toggleFilter()" @click.stop="toggleFilter()"
> >
<v-icon :class="`${isolated ? 'primary--text' : 'grey--text'}`" style="font-size: 12px"> <v-icon
:class="`${isolated ? 'primary--text' : 'grey--text'}`"
style="font-size: 12px"
>
{{ !isolated ? 'mdi-filter' : 'mdi-filter' }} {{ !isolated ? 'mdi-filter' : 'mdi-filter' }}
</v-icon> </v-icon>
</v-btn> </v-btn>
<v-btn v-tooltip="'Expand/collapse property'" x-small icon @click.stop="expanded = !expanded"> <v-btn
<v-icon :class="`${expanded ? 'grey--text' : 'primary--text'}`" style="font-size: 12px"> v-tooltip="'Expand/collapse property'"
x-small
icon
@click.stop="expanded = !expanded"
>
<v-icon
:class="`${expanded ? 'grey--text' : 'primary--text'}`"
style="font-size: 12px"
>
{{ expanded ? 'mdi-minus' : 'mdi-plus' }} {{ expanded ? 'mdi-minus' : 'mdi-plus' }}
</v-icon> </v-icon>
</v-btn> </v-btn>
</v-col> </v-col>
<v-scroll-y-transition> <v-scroll-y-transition>
<v-col v-if="expanded && (prop.type === 'object' || prop.type === 'array')" cols="12"> <v-col
v-if="expanded && (prop.type === 'object' || prop.type === 'array')"
cols="12"
>
<object-properties :obj="prop.value" :stream-id="streamId" /> <object-properties :obj="prop.value" :stream-id="streamId" />
</v-col> </v-col>
</v-scroll-y-transition> </v-scroll-y-transition>
@@ -129,7 +155,9 @@ export default {
} }
if (this.prop.type === 'array') { if (this.prop.type === 'array') {
let ids = this.prop.value.map((o) => o.referencedId) let ids = this.prop.value.map((o) => o.referencedId)
let targetIds = this.$store.state.hideValues.filter((val) => ids.indexOf(val) !== -1) let targetIds = this.$store.state.hideValues.filter(
(val) => ids.indexOf(val) !== -1
)
if (targetIds.length === 0) return true if (targetIds.length === 0) return true
else return false // return "partial" or "full", depending on state else return false // return "partial" or "full", depending on state
} }
@@ -137,11 +165,15 @@ export default {
}, },
isolated() { isolated() {
if (this.prop.type === 'object') { if (this.prop.type === 'object') {
return this.$store.state.isolateValues.indexOf(this.prop.value.referencedId) !== -1 return (
this.$store.state.isolateValues.indexOf(this.prop.value.referencedId) !== -1
)
} }
if (this.prop.type === 'array') { if (this.prop.type === 'array') {
let ids = this.prop.value.map((o) => o.referencedId) let ids = this.prop.value.map((o) => o.referencedId)
let targetIds = this.$store.state.isolateValues.filter((val) => ids.indexOf(val) !== -1) let targetIds = this.$store.state.isolateValues.filter(
(val) => ids.indexOf(val) !== -1
)
if (targetIds.length === 0) return false if (targetIds.length === 0) return false
else return true // return "partial" or "full", depending on state else return true // return "partial" or "full", depending on state
} }
@@ -158,8 +190,15 @@ export default {
} }
if (this.visible) if (this.visible)
this.$store.commit('hideObjects', { filterKey: '__parents', filterValues: targetIds }) this.$store.commit('hideObjects', {
else this.$store.commit('showObjects', { filterKey: '__parents', filterValues: targetIds }) filterKey: '__parents',
filterValues: targetIds
})
else
this.$store.commit('showObjects', {
filterKey: '__parents',
filterValues: targetIds
})
}, },
toggleFilter() { toggleFilter() {
let targetIds let targetIds
@@ -169,8 +208,15 @@ export default {
} }
if (this.isolated) if (this.isolated)
this.$store.commit('unisolateObjects', { filterKey: '__parents', filterValues: targetIds }) this.$store.commit('unisolateObjects', {
else this.$store.commit('isolateObjects', { filterKey: '__parents', filterValues: targetIds }) filterKey: '__parents',
filterValues: targetIds
})
else
this.$store.commit('isolateObjects', {
filterKey: '__parents',
filterValues: targetIds
})
} }
} }
} }
@@ -27,7 +27,9 @@
icon icon
@click.stop="isolateSelection()" @click.stop="isolateSelection()"
> >
<v-icon x-small :class="`${!isolated ? 'primary--text' : ''}`">mdi-filter</v-icon> <v-icon x-small :class="`${!isolated ? 'primary--text' : ''}`">
mdi-filter
</v-icon>
</v-btn> </v-btn>
<v-btn <v-btn
v-show="$vuetify.breakpoint.xs" v-show="$vuetify.breakpoint.xs"
@@ -40,11 +42,21 @@
</v-btn> </v-btn>
</div> </div>
<div v-for="prop in props" :key="prop.value.id" style="width: 99%"> <div v-for="prop in props" :key="prop.value.id" style="width: 99%">
<v-card class="transparent elevation-3 rounded-lg mb-3" style="pointer-events: auto"> <v-card
<object-properties-row :prop="prop" :stream-id="streamId" :ref-id="prop.refId" /> class="transparent elevation-3 rounded-lg mb-3"
style="pointer-events: auto"
>
<object-properties-row
:prop="prop"
:stream-id="streamId"
:ref-id="prop.refId"
/>
</v-card> </v-card>
</div> </div>
<div v-show="props.length === 1 && !$vuetify.breakpoint.xs" class="caption grey--text"> <div
v-show="props.length === 1 && !$vuetify.breakpoint.xs"
class="caption grey--text"
>
Hint: hold shift to select multiple objects. Hint: hold shift to select multiple objects.
</div> </div>
</perfect-scrollbar> </perfect-scrollbar>
@@ -98,12 +110,21 @@ export default {
isolateSelection() { isolateSelection() {
let ids = this.objects.map((o) => o.id) let ids = this.objects.map((o) => o.id)
if (!this.isolated) if (!this.isolated)
this.$store.commit('unisolateObjects', { filterKey: '__parents', filterValues: ids }) this.$store.commit('unisolateObjects', {
else this.$store.commit('isolateObjects', { filterKey: '__parents', filterValues: ids }) filterKey: '__parents',
filterValues: ids
})
else
this.$store.commit('isolateObjects', {
filterKey: '__parents',
filterValues: ids
})
}, },
getSelectionUrl() { getSelectionUrl() {
if (this.objects.length < 2) return '' if (this.objects.length < 2) return ''
let url = `/streams/${this.streamId}/objects/${this.objects[0].id}?overlay=${this.objects let url = `/streams/${this.streamId}/objects/${
this.objects[0].id
}?overlay=${this.objects
.slice(1) .slice(1)
.map((o) => o.id) .map((o) => o.id)
.join(',')}` .join(',')}`
@@ -50,7 +50,11 @@
/> --> /> -->
<div v-show="removedResources.length !== 0" class="px-3 caption pb-5"> <div v-show="removedResources.length !== 0" class="px-3 caption pb-5">
Removed resources: Removed resources:
<span v-for="(res, index) in removedResources" :key="index" v-tooltip="'Click to re-add'"> <span
v-for="(res, index) in removedResources"
:key="index"
v-tooltip="'Click to re-add'"
>
<a <a
@click=" @click="
$emit('add-resource', res.id) $emit('add-resource', res.id)
@@ -62,10 +66,16 @@
> >
<span v-if="res.type === 'object'">Object</span> <span v-if="res.type === 'object'">Object</span>
<!-- eslint-disable-next-line prettier/prettier --> <!-- eslint-disable-next-line prettier/prettier -->
<span v-else><v-icon x-small>mdi-source-commit</v-icon>{{ res.id }}</span> <span v-else>
<v-icon x-small>mdi-source-commit</v-icon>
{{ res.id }}
</span>
</a> </a>
<!-- eslint-disable-next-line prettier/prettier --> <!-- eslint-disable-next-line prettier/prettier -->
<span v-if="removedResources.length > 1 && index < removedResources.length - 1">,&nbsp; <span
v-if="removedResources.length > 1 && index < removedResources.length - 1"
>
,&nbsp;
</span> </span>
</span> </span>
</div> </div>
@@ -28,7 +28,14 @@
<v-icon small>mdi-perspective-less</v-icon> <v-icon small>mdi-perspective-less</v-icon>
</v-btn> --> </v-btn> -->
<canonical-views :small="small" /> <canonical-views :small="small" />
<v-btn v-tooltip="'Zoom extents'" :small="small" rounded icon class="mr-2" @click="zoomEx()"> <v-btn
v-tooltip="'Zoom extents'"
:small="small"
rounded
icon
class="mr-2"
@click="zoomEx()"
>
<v-icon small>mdi-arrow-expand</v-icon> <v-icon small>mdi-arrow-expand</v-icon>
</v-btn> </v-btn>
<v-btn <v-btn
@@ -42,7 +49,11 @@
<v-icon small>mdi-scissors-cutting</v-icon> <v-icon small>mdi-scissors-cutting</v-icon>
</v-btn> </v-btn>
<!-- Other components teleport extra controls in here --> <!-- Other components teleport extra controls in here -->
<portal-target name="viewercontrols" class="d-flex align-center" multiple></portal-target> <portal-target
name="viewercontrols"
class="d-flex align-center"
multiple
></portal-target>
</v-card> </v-card>
</div> </div>
</template> </template>
@@ -14,7 +14,11 @@
:url="`/preview/${streamId}/commits/${commit.id}`" :url="`/preview/${streamId}/commits/${commit.id}`"
></preview-image> ></preview-image>
<div style="position: absolute; top: 10px; right: 20px"> <div style="position: absolute; top: 10px; right: 20px">
<commit-received-receipts :stream-id="streamId" :commit-id="commit.id" shadow /> <commit-received-receipts
:stream-id="streamId"
:commit-id="commit.id"
shadow
/>
</div> </div>
<div style="position: absolute; top: 10px; left: 12px"> <div style="position: absolute; top: 10px; left: 12px">
<source-app-avatar :application-name="commit.sourceApplication" /> <source-app-avatar :application-name="commit.sourceApplication" />
@@ -48,14 +52,15 @@ export default {
InfiniteLoading: () => import('vue-infinite-loading'), InfiniteLoading: () => import('vue-infinite-loading'),
ListItemCommit: () => import('@/main/components/stream/ListItemCommit'), ListItemCommit: () => import('@/main/components/stream/ListItemCommit'),
PreviewImage: () => import('@/main/components/common/PreviewImage'), PreviewImage: () => import('@/main/components/common/PreviewImage'),
CommitReceivedReceipts: () => import('@/main/components/common/CommitReceivedReceipts'), CommitReceivedReceipts: () =>
import('@/main/components/common/CommitReceivedReceipts'),
SourceAppAvatar: () => import('@/main/components/common/SourceAppAvatar') SourceAppAvatar: () => import('@/main/components/common/SourceAppAvatar')
}, },
props: ['streamId'], props: ['streamId'],
apollo: { apollo: {
stream: { stream: {
query: gql` query: gql`
query($streamId: String!, $cursor: String) { query ($streamId: String!, $cursor: String) {
stream(id: $streamId) { stream(id: $streamId) {
id id
commits(cursor: $cursor, limit: 2) { commits(cursor: $cursor, limit: 2) {
@@ -101,7 +106,8 @@ export default {
let allItems = [...previousResult.stream.commits.items] let allItems = [...previousResult.stream.commits.items]
for (const commit of newItems) { for (const commit of newItems) {
if (allItems.findIndex((c) => c.id === commit.id) === -1) allItems.push(commit) if (allItems.findIndex((c) => c.id === commit.id) === -1)
allItems.push(commit)
} }
return { return {
@@ -1,14 +1,24 @@
<template> <template>
<div> <div>
<v-row v-if="commits.length != 0" dense> <v-row v-if="commits.length != 0" dense>
<v-col v-for="commit in commits" :key="commit.id + 'card'" cols="12" sm="6" md="4"> <v-col
v-for="commit in commits"
:key="commit.id + 'card'"
cols="12"
sm="6"
md="4"
>
<v-card @click.stop="$emit('add-resource', commit.id)"> <v-card @click.stop="$emit('add-resource', commit.id)">
<preview-image <preview-image
:height="180" :height="180"
:url="`/preview/${streamId}/commits/${commit.id}`" :url="`/preview/${streamId}/commits/${commit.id}`"
></preview-image> ></preview-image>
<div style="position: absolute; top: 10px; right: 20px"> <div style="position: absolute; top: 10px; right: 20px">
<commit-received-receipts :stream-id="streamId" :commit-id="commit.id" shadow /> <commit-received-receipts
:stream-id="streamId"
:commit-id="commit.id"
shadow
/>
</div> </div>
<div style="position: absolute; top: 10px; left: 12px"> <div style="position: absolute; top: 10px; left: 12px">
<source-app-avatar :application-name="commit.sourceApplication" /> <source-app-avatar :application-name="commit.sourceApplication" />
@@ -43,7 +53,8 @@ export default {
InfiniteLoading: () => import('vue-infinite-loading'), InfiniteLoading: () => import('vue-infinite-loading'),
ListItemCommit: () => import('@/main/components/stream/ListItemCommit'), ListItemCommit: () => import('@/main/components/stream/ListItemCommit'),
PreviewImage: () => import('@/main/components/common/PreviewImage'), PreviewImage: () => import('@/main/components/common/PreviewImage'),
CommitReceivedReceipts: () => import('@/main/components/common/CommitReceivedReceipts'), CommitReceivedReceipts: () =>
import('@/main/components/common/CommitReceivedReceipts'),
SourceAppAvatar: () => import('@/main/components/common/SourceAppAvatar') SourceAppAvatar: () => import('@/main/components/common/SourceAppAvatar')
}, },
props: ['streamId', 'branchName'], props: ['streamId', 'branchName'],
@@ -20,10 +20,14 @@
required required
></v-text-field> ></v-text-field>
<p class="caption"> <p class="caption">
Tip: you can create nested branches by using "/" as a separator in their names. E.g., Tip: you can create nested branches by using "/" as a separator in their
"mep/stage-1" or "arch/sketch-design". names. E.g., "mep/stage-1" or "arch/sketch-design".
</p> </p>
<v-textarea v-model="editableBranch.description" rows="2" label="Description"></v-textarea> <v-textarea
v-model="editableBranch.description"
rows="2"
label="Description"
></v-textarea>
</v-card-text> </v-card-text>
</v-form> </v-form>
<v-card-actions> <v-card-actions>
@@ -43,7 +47,9 @@
</v-app-bar-nav-icon> </v-app-bar-nav-icon>
<v-toolbar-title>Delete Branch</v-toolbar-title> <v-toolbar-title>Delete Branch</v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon @click="showDeleteDialog = false"><v-icon>mdi-close</v-icon></v-btn> <v-btn icon @click="showDeleteDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar> </v-toolbar>
<v-card-text class="mt-4"> <v-card-text class="mt-4">
You cannot undo this action. The branch You cannot undo this action. The branch
@@ -90,8 +96,12 @@ export default {
nameRules: [ nameRules: [
(v) => !!v || 'Name is required.', (v) => !!v || 'Name is required.',
(v) => (v) =>
!(v.startsWith('#') || v.endsWith('#') || v.startsWith('/') || v.endsWith('/')) || !(
'Branch names cannot start or end with "#" or "/"', v.startsWith('#') ||
v.endsWith('#') ||
v.startsWith('/') ||
v.endsWith('/')
) || 'Branch names cannot start or end with "#" or "/"',
(v) => (v) =>
(v && this.allBranchNames.findIndex((e) => e === v) === -1) || (v && this.allBranchNames.findIndex((e) => e === v) === -1) ||
'A branch with this name already exists', 'A branch with this name already exists',
@@ -200,11 +210,18 @@ export default {
text: 'Branch updated', text: 'Branch updated',
action: { action: {
name: 'View', name: 'View',
to: `/streams/` + this.$route.params.streamId + `/branches/` + this.editableBranch.name to:
`/streams/` +
this.$route.params.streamId +
`/branches/` +
this.editableBranch.name
} }
}) })
this.$router.push( this.$router.push(
`/streams/` + this.$route.params.streamId + `/branches/` + this.editableBranch.name `/streams/` +
this.$route.params.streamId +
`/branches/` +
this.editableBranch.name
) )
this.$emit('close') this.$emit('close')
} }
@@ -48,11 +48,13 @@
</v-app-bar-nav-icon> </v-app-bar-nav-icon>
<v-toolbar-title>Delete Commit</v-toolbar-title> <v-toolbar-title>Delete Commit</v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon @click="showDeleteDialog = false"><v-icon>mdi-close</v-icon></v-btn> <v-btn icon @click="showDeleteDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar> </v-toolbar>
<v-card-text class="mt-4"> <v-card-text class="mt-4">
You cannot undo this action. This will permanently delete this commit. To confirm, type in You cannot undo this action. This will permanently delete this commit. To
its its id ( confirm, type in its its id (
<code>{{ commit.id }}</code> <code>{{ commit.id }}</code>
) below: ) below:
<v-text-field <v-text-field
@@ -109,7 +111,11 @@ export default {
} }
`, `,
variables: { variables: {
myCommit: { streamId: this.stream.id, id: this.commit.id, message: this.commit.message } myCommit: {
streamId: this.stream.id,
id: this.commit.id,
message: this.commit.message
}
} }
}) })
} catch (err) { } catch (err) {
@@ -143,7 +149,9 @@ export default {
text: err.message text: err.message
}) })
} }
this.$router.push(`/streams/` + this.$route.params.streamId + `/branches/` + commitBranch) this.$router.push(
`/streams/` + this.$route.params.streamId + `/branches/` + commitBranch
)
this.loading = false this.loading = false
this.showDeleteDialog = false this.showDeleteDialog = false
} }
@@ -22,8 +22,8 @@
autofocus autofocus
></v-text-field> ></v-text-field>
<p class="caption"> <p class="caption">
Tip: you can create nested branches by using "/" as a separator in their names. E.g., Tip: you can create nested branches by using "/" as a separator in their
"mep/stage-1" or "arch/sketch-design". names. E.g., "mep/stage-1" or "arch/sketch-design".
</p> </p>
<v-textarea v-model="description" rows="2" label="Description"></v-textarea> <v-textarea v-model="description" rows="2" label="Description"></v-textarea>
</v-card-text> </v-card-text>
@@ -50,7 +50,8 @@ export default {
nameRules: [ nameRules: [
(v) => !!v || 'Branches need a name too!', (v) => !!v || 'Branches need a name too!',
(v) => (v) =>
!(v.startsWith('#') || v.startsWith('/')) || 'Branch names cannot start with "#" or "/"', !(v.startsWith('#') || v.startsWith('/')) ||
'Branch names cannot start with "#" or "/"',
(v) => (v) =>
(v && this.reservedBranchNames.findIndex((e) => e === v) === -1) || (v && this.reservedBranchNames.findIndex((e) => e === v) === -1) ||
'This is a reserved branch name', 'This is a reserved branch name',
@@ -8,7 +8,13 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon @click="$emit('close')"><v-icon>mdi-close</v-icon></v-btn> <v-btn icon @click="$emit('close')"><v-icon>mdi-close</v-icon></v-btn>
</v-toolbar> </v-toolbar>
<v-form ref="form" v-model="valid" lazy-validation class="px-2" @submit.prevent="createStream"> <v-form
ref="form"
v-model="valid"
lazy-validation
class="px-2"
@submit.prevent="createStream"
>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="name" v-model="name"
@@ -17,7 +23,12 @@
autofocus autofocus
label="Stream Name (optional)" label="Stream Name (optional)"
/> />
<v-textarea v-model="description" rows="1" row-height="15" label="Description (optional)" /> <v-textarea
v-model="description"
rows="1"
row-height="15"
label="Description (optional)"
/>
<v-switch <v-switch
v-model="isPublic" v-model="isPublic"
v-tooltip=" v-tooltip="
@@ -46,7 +57,11 @@
<v-list-item-subtitle>Try a different search query.</v-list-item-subtitle> <v-list-item-subtitle>Try a different search query.</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<v-list-item v-for="item in userSearch.items" :key="item.id" @click="addCollab(item)"> <v-list-item
v-for="item in userSearch.items"
:key="item.id"
@click="addCollab(item)"
>
<v-list-item-avatar> <v-list-item-avatar>
<user-avatar <user-avatar
:id="item.id" :id="item.id"
@@ -218,7 +233,8 @@ export default {
} }
} }
this.$emit('created') this.$emit('created')
if (this.redirect) this.$router.push({ path: `/streams/${res.data.streamCreate}` }) if (this.redirect)
this.$router.push({ path: `/streams/${res.data.streamCreate}` })
} catch (e) { } catch (e) {
this.$eventHub.$emit('notification', { this.$eventHub.$emit('notification', {
text: e.message text: e.message
@@ -8,7 +8,12 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon @click="$emit('close')"><v-icon>mdi-close</v-icon></v-btn> <v-btn icon @click="$emit('close')"><v-icon>mdi-close</v-icon></v-btn>
</v-toolbar> </v-toolbar>
<v-alert v-model="showError" dismissible type="error" :class="`${success ? 'mb-0' : ''}`"> <v-alert
v-model="showError"
dismissible
type="error"
:class="`${success ? 'mb-0' : ''}`"
>
{{ error }} {{ error }}
</v-alert> </v-alert>
<v-alert v-model="success" timeout="3000" dismissible type="success"> <v-alert v-model="success" timeout="3000" dismissible type="success">
@@ -16,11 +21,15 @@
</v-alert> </v-alert>
<v-form ref="form" v-model="valid" class="px-2" @submit.prevent="sendInvite"> <v-form ref="form" v-model="valid" class="px-2" @submit.prevent="sendInvite">
<v-card-text class="pb-0 mb-0"> <v-card-text class="pb-0 mb-0">
Speckle will send a server invite link to the email below. You can also add a personal Speckle will send a server invite link to the email below. You can also add a
message if you want to. personal message if you want to.
</v-card-text> </v-card-text>
<v-card-text class="pt-0 mt-0"> <v-card-text class="pt-0 mt-0">
<v-text-field v-model="email" :rules="validation.emailRules" label="email"></v-text-field> <v-text-field
v-model="email"
:rules="validation.emailRules"
label="email"
></v-text-field>
<v-text-field <v-text-field
v-model="message" v-model="message"
:rules="validation.messageRules" :rules="validation.messageRules"
@@ -85,7 +94,7 @@ export default {
try { try {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: gql` mutation: gql`
mutation($input: ServerInviteCreateInput!) { mutation ($input: ServerInviteCreateInput!) {
serverInviteCreate(input: $input) serverInviteCreate(input: $input)
} }
`, `,
@@ -34,7 +34,10 @@
@focus="copyToClipboard" @focus="copyToClipboard"
></v-text-field> ></v-text-field>
<v-text-field <v-text-field
v-if="$route.params.resourceId && $resourceType($route.params.resourceId) === 'commit'" v-if="
$route.params.resourceId &&
$resourceType($route.params.resourceId) === 'commit'
"
ref="commitUrl" ref="commitUrl"
dark dark
filled filled
@@ -73,7 +76,8 @@
<div v-if="stream.isPublic"> <div v-if="stream.isPublic">
<v-card-text> <v-card-text>
<div class="caption mx-1 pb-2"> <div class="caption mx-1 pb-2">
Copy the code below to embed an iframe of this model in your webpage or document. Copy the code below to embed an iframe of this model in your webpage or
document.
</div> </div>
<v-text-field <v-text-field
dense dense
@@ -86,7 +90,10 @@
</v-card-text> </v-card-text>
</div> </div>
</v-sheet> </v-sheet>
<v-sheet v-if="stream" :class="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"> <v-sheet
v-if="stream"
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
>
<v-toolbar v-if="stream.role === 'stream:owner'" class="transparent" rounded flat> <v-toolbar v-if="stream.role === 'stream:owner'" class="transparent" rounded flat>
<v-app-bar-nav-icon style="pointer-events: none"> <v-app-bar-nav-icon style="pointer-events: none">
<v-icon>{{ stream.isPublic ? 'mdi-lock-open' : 'mdi-lock' }}</v-icon> <v-icon>{{ stream.isPublic ? 'mdi-lock-open' : 'mdi-lock' }}</v-icon>
@@ -104,7 +111,8 @@
></v-switch> ></v-switch>
</v-toolbar> </v-toolbar>
<v-card-text v-if="stream.isPublic" class="pt-2"> <v-card-text v-if="stream.isPublic" class="pt-2">
This stream is public. This means that anyone with the link can view and read data from it. This stream is public. This means that anyone with the link can view and read
data from it.
</v-card-text> </v-card-text>
<v-card-text v-if="!stream.isPublic" class="pt-2 pb-2"> <v-card-text v-if="!stream.isPublic" class="pt-2 pb-2">
This stream is private. This means that only collaborators can access it. This stream is private. This means that only collaborators can access it.
@@ -115,7 +123,9 @@
v-tooltip=" v-tooltip="
`${ `${
stream.role !== 'stream:owner' stream.role !== 'stream:owner'
? 'You do not have the right access level (' + stream.role + ') to add collaborators.' ? 'You do not have the right access level (' +
stream.role +
') to add collaborators.'
: '' : ''
}` }`
" "
@@ -127,7 +137,10 @@
<v-toolbar-title> <v-toolbar-title>
Collaborators Collaborators
<user-avatar <user-avatar
v-for="collab in stream.collaborators.slice(0, stream.collaborators.length > 5 ? 4 : 5)" v-for="collab in stream.collaborators.slice(
0,
stream.collaborators.length > 5 ? 4 : 5
)"
:id="collab.id" :id="collab.id"
:key="collab.id" :key="collab.id"
:size="20" :size="20"
@@ -11,7 +11,12 @@
<v-btn icon @click="showDialog = false"><v-icon>mdi-close</v-icon></v-btn> <v-btn icon @click="showDialog = false"><v-icon>mdi-close</v-icon></v-btn>
</v-toolbar> </v-toolbar>
<v-alert v-model="showError" dismissible type="error" :class="`${success ? 'mb-0' : ''}`"> <v-alert
v-model="showError"
dismissible
type="error"
:class="`${success ? 'mb-0' : ''}`"
>
{{ error }} {{ error }}
</v-alert> </v-alert>
<v-alert v-model="success" dismissible type="success"> <v-alert v-model="success" dismissible type="success">
@@ -19,8 +24,8 @@
</v-alert> </v-alert>
<v-form ref="form" v-model="valid" class="px-2" @submit.prevent="sendInvite"> <v-form ref="form" v-model="valid" class="px-2" @submit.prevent="sendInvite">
<v-card-text class="pb-0 mb-0"> <v-card-text class="pb-0 mb-0">
We will send an invite to the email below - once they accept, they will also gain access We will send an invite to the email below - once they accept, they will also
to this stream! gain access to this stream!
</v-card-text> </v-card-text>
<v-card-text class="pt-0 mt-0"> <v-card-text class="pt-0 mt-0">
<v-text-field <v-text-field
@@ -117,7 +122,7 @@ export default {
try { try {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: gql` mutation: gql`
mutation($input: StreamInviteCreateInput!) { mutation ($input: StreamInviteCreateInput!) {
streamInviteCreate(input: $input) streamInviteCreate(input: $input)
} }
`, `,
@@ -15,8 +15,8 @@
<v-row> <v-row>
<v-col cols="12" class="pb-0"> <v-col cols="12" class="pb-0">
<p> <p>
To protect against accidental deletion, please enter the email address associated To protect against accidental deletion, please enter the email address
with this account: associated with this account:
</p> </p>
</v-col> </v-col>
</v-row> </v-row>
@@ -38,7 +38,9 @@
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn text @click="cancel">Cancel</v-btn> <v-btn text @click="cancel">Cancel</v-btn>
<v-btn color="error" :disabled="!valid" type="submit">Delete my account</v-btn> <v-btn color="error" :disabled="!valid" type="submit">
Delete my account
</v-btn>
</v-card-actions> </v-card-actions>
</v-form> </v-form>
</v-card> </v-card>
@@ -28,7 +28,11 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" class="pt-0 pb-0"> <v-col cols="12" class="pt-0 pb-0">
<v-text-field v-model="user.company" filled label="Company"></v-text-field> <v-text-field
v-model="user.company"
filled
label="Company"
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
@@ -64,7 +68,9 @@ export default {
(v) => !!v || 'Name is required', (v) => !!v || 'Name is required',
(v) => (v && v.length <= 60) || 'Name must be less than 60 characters.' (v) => (v && v.length <= 60) || 'Name must be less than 60 characters.'
], ],
bioRules: [(v) => !v || (v && v.length <= 500) || 'Bio must be less than 500 characters.'], bioRules: [
(v) => !v || (v && v.length <= 500) || 'Bio must be less than 500 characters.'
],
valid: true valid: true
} }
}, },
+8 -1
View File
@@ -2,7 +2,14 @@
<v-app :class="`${$vuetify.theme.dark ? 'background-dark' : 'background-light'}`"> <v-app :class="`${$vuetify.theme.dark ? 'background-dark' : 'background-light'}`">
<v-container fill-height fluid> <v-container fill-height fluid>
<v-row align="center" justify="center"> <v-row align="center" justify="center">
<v-col v-if="showBlurb" cols="12" md="6" lg="6" xl="4" class="hidden-sm-and-down"> <v-col
v-if="showBlurb"
cols="12"
md="6"
lg="6"
xl="4"
class="hidden-sm-and-down"
>
<blurb :server-info="serverInfo" /> <blurb :server-info="serverInfo" />
</v-col> </v-col>
<v-col cols="11" sm="8" md="6" lg="4" xl="3"> <v-col cols="11" sm="8" md="6" lg="4" xl="3">
+18 -5
View File
@@ -12,7 +12,10 @@
<div v-show="$route.meta.resizableNavbar" class="nav-resizer"></div> <div v-show="$route.meta.resizableNavbar" class="nav-resizer"></div>
<main-nav :expanded="drawer" @hide-drawer="drawer = false" /> <main-nav :expanded="drawer" @hide-drawer="drawer = false" />
<template #append> <template #append>
<div :xxxstyle="`${$isMobile() ? 'padding-bottom: 58px' : ''}`" class="elevation-10"> <div
:xxxstyle="`${$isMobile() ? 'padding-bottom: 58px' : ''}`"
class="elevation-10"
>
<main-nav-bottom /> <main-nav-bottom />
</div> </div>
</template> </template>
@@ -70,7 +73,8 @@ export default {
SearchBar: () => import('@/main/components/common/SearchBar'), SearchBar: () => import('@/main/components/common/SearchBar'),
GlobalToast: () => import('@/main/components/common/GlobalToast'), GlobalToast: () => import('@/main/components/common/GlobalToast'),
GlobalLoading: () => import('@/main/components/common/GlobalLoading'), GlobalLoading: () => import('@/main/components/common/GlobalLoading'),
EmailVerificationBanner: () => import('@/main/components/user/EmailVerificationBanner') EmailVerificationBanner: () =>
import('@/main/components/user/EmailVerificationBanner')
}, },
apollo: { apollo: {
serverInfo: { serverInfo: {
@@ -129,7 +133,10 @@ export default {
let mixpanelId = this.$mixpanelId() let mixpanelId = this.$mixpanelId()
if (mixpanelId !== null) { if (mixpanelId !== null) {
this.$mixpanel.identify(mixpanelId) this.$mixpanel.identify(mixpanelId)
this.$mixpanel.people.set('Theme Web', this.$vuetify.theme.dark ? 'dark' : 'light') this.$mixpanel.people.set(
'Theme Web',
this.$vuetify.theme.dark ? 'dark' : 'light'
)
this.$mixpanel.people.set('Identified', true) this.$mixpanel.people.set('Identified', true)
} }
this.$mixpanel.track('Visit Web App') this.$mixpanel.track('Visit Web App')
@@ -137,8 +144,14 @@ export default {
methods: { methods: {
switchTheme() { switchTheme() {
this.$vuetify.theme.dark = !this.$vuetify.theme.dark this.$vuetify.theme.dark = !this.$vuetify.theme.dark
localStorage.setItem('darkModeEnabled', this.$vuetify.theme.dark ? 'dark' : 'light') localStorage.setItem(
this.$mixpanel.people.set('Theme Web', this.$vuetify.theme.dark ? 'dark' : 'light') 'darkModeEnabled',
this.$vuetify.theme.dark ? 'dark' : 'light'
)
this.$mixpanel.people.set(
'Theme Web',
this.$vuetify.theme.dark ? 'dark' : 'light'
)
}, },
setNavResizeEvents() { setNavResizeEvents() {
const minSize = this.borderSize const minSize = this.borderSize
@@ -1,6 +1,10 @@
<template> <template>
<div> <div>
<main-logo :shadow="shadowSpeckle" :expanded="expanded" @hide-drawer="$emit('hide-drawer')" /> <main-logo
:shadow="shadowSpeckle"
:expanded="expanded"
@hide-drawer="$emit('hide-drawer')"
/>
<portal-target name="nav"> <portal-target name="nav">
<!-- Main Actions --> <!-- Main Actions -->
@@ -40,7 +44,9 @@
<template slot="activator"> <template slot="activator">
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Streams</v-list-item-title> <v-list-item-title>Streams</v-list-item-title>
<v-list-item-subtitle class="caption">All your streams</v-list-item-subtitle> <v-list-item-subtitle class="caption">
All your streams
</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</template> </template>
@@ -62,7 +68,9 @@
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Commits</v-list-item-title> <v-list-item-title>Commits</v-list-item-title>
<v-list-item-subtitle class="caption">Your latest commits</v-list-item-subtitle> <v-list-item-subtitle class="caption">
Your latest commits
</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<portal-target name="subnav-commits" /> <portal-target name="subnav-commits" />
@@ -72,7 +80,9 @@
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Admin</v-list-item-title> <v-list-item-title>Admin</v-list-item-title>
<v-list-item-subtitle class="caption">Server Management</v-list-item-subtitle> <v-list-item-subtitle class="caption">
Server Management
</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<portal-target name="subnav-admin" /> <portal-target name="subnav-admin" />
@@ -89,7 +99,9 @@
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Profile</v-list-item-title> <v-list-item-title>Profile</v-list-item-title>
<v-list-item-subtitle class="caption">Settings & Security</v-list-item-subtitle> <v-list-item-subtitle class="caption">
Settings & Security
</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<portal-target name="subnav-profile" /> <portal-target name="subnav-profile" />
@@ -98,11 +110,19 @@
<!-- Dialogs --> <!-- Dialogs -->
<v-dialog v-model="newStreamDialog" max-width="500" :fullscreen="$vuetify.breakpoint.xsOnly"> <v-dialog
v-model="newStreamDialog"
max-width="500"
:fullscreen="$vuetify.breakpoint.xsOnly"
>
<new-stream @created="newStreamDialog = false" @close="newStreamDialog = false" /> <new-stream @created="newStreamDialog = false" @close="newStreamDialog = false" />
</v-dialog> </v-dialog>
<v-dialog v-model="inviteUsersDialog" max-width="500" :fullscreen="$vuetify.breakpoint.xsOnly"> <v-dialog
v-model="inviteUsersDialog"
max-width="500"
:fullscreen="$vuetify.breakpoint.xsOnly"
>
<server-invites @close="inviteUsersDialog = false" /> <server-invites @close="inviteUsersDialog = false" />
</v-dialog> </v-dialog>
</div> </div>
@@ -117,7 +137,10 @@ export default {
ServerInvites: () => import('@/main/dialogs/ServerInvites'), ServerInvites: () => import('@/main/dialogs/ServerInvites'),
UserAvatarIcon: () => import('@/main/components/common/UserAvatarIcon') UserAvatarIcon: () => import('@/main/components/common/UserAvatarIcon')
}, },
props: { expanded: { type: Boolean, default: false }, drawer: { type: Boolean, default: true } }, props: {
expanded: { type: Boolean, default: false },
drawer: { type: Boolean, default: true }
},
apollo: { apollo: {
user: { user: {
query: MainUserDataQuery query: MainUserDataQuery
@@ -131,7 +154,9 @@ export default {
} }
}, },
mounted() { mounted() {
let navContent = [...document.getElementsByClassName('v-navigation-drawer__content')][0] let navContent = [
...document.getElementsByClassName('v-navigation-drawer__content')
][0]
navContent.addEventListener('scroll', () => { navContent.addEventListener('scroll', () => {
if (navContent.scrollTop > 50) this.shadowSpeckle = true if (navContent.scrollTop > 50) this.shadowSpeckle = true
else this.shadowSpeckle = false else this.shadowSpeckle = false
@@ -141,8 +166,14 @@ export default {
methods: { methods: {
switchTheme() { switchTheme() {
this.$vuetify.theme.dark = !this.$vuetify.theme.dark this.$vuetify.theme.dark = !this.$vuetify.theme.dark
localStorage.setItem('darkModeEnabled', this.$vuetify.theme.dark ? 'dark' : 'light') localStorage.setItem(
this.$mixpanel.people.set('Theme Web', this.$vuetify.theme.dark ? 'dark' : 'light') 'darkModeEnabled',
this.$vuetify.theme.dark ? 'dark' : 'light'
)
this.$mixpanel.people.set(
'Theme Web',
this.$vuetify.theme.dark ? 'dark' : 'light'
)
} }
} }
} }
@@ -19,7 +19,11 @@
<v-col> <v-col>
<v-btn x-small block depressed @click="switchTheme()"> <v-btn x-small block depressed @click="switchTheme()">
<v-icon x-small class="mr-1"> <v-icon x-small class="mr-1">
{{ $vuetify.theme.dark ? 'mdi-white-balance-sunny' : 'mdi-weather-night' }} {{
$vuetify.theme.dark
? 'mdi-white-balance-sunny'
: 'mdi-weather-night'
}}
</v-icon> </v-icon>
<!-- {{ $vuetify.theme.dark ? 'mdi-white-balance-sunny' : 'mdi-weather-night' }} --> <!-- {{ $vuetify.theme.dark ? 'mdi-white-balance-sunny' : 'mdi-weather-night' }} -->
</v-btn> </v-btn>
@@ -64,8 +68,14 @@ export default {
}, },
switchTheme() { switchTheme() {
this.$vuetify.theme.dark = !this.$vuetify.theme.dark this.$vuetify.theme.dark = !this.$vuetify.theme.dark
localStorage.setItem('darkModeEnabled', this.$vuetify.theme.dark ? 'dark' : 'light') localStorage.setItem(
this.$mixpanel.people.set('Theme Web', this.$vuetify.theme.dark ? 'dark' : 'light') 'darkModeEnabled',
this.$vuetify.theme.dark ? 'dark' : 'light'
)
this.$mixpanel.people.set(
'Theme Web',
this.$vuetify.theme.dark ? 'dark' : 'light'
)
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More