Merge branch 'main' of github.com:specklesystems/speckle-server into ams3_ci
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
name: Update issue Status
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
update_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ORGANIZATION: specklesystems
|
||||
PROJECT_NUMBER: 9
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
query($org: String!, $number: Int!) {
|
||||
organization(login: $org){
|
||||
projectNext(number: $number) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
echo "$PROJECT_ID"
|
||||
echo "$STATUS_FIELD_ID"
|
||||
|
||||
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo "$DONE_ID"
|
||||
|
||||
- name: Add Issue to project #it's already in the project, but we do this to get its node id!
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $id:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Update Status
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $status:ID!, $id:ID!, $value:String!) {
|
||||
set_status: updateProjectNextItemField(
|
||||
input: {
|
||||
projectId: $project
|
||||
itemId: $id
|
||||
fieldId: $status
|
||||
value: $value
|
||||
}
|
||||
) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Move new issues into Project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ORGANIZATION: specklesystems
|
||||
PROJECT_NUMBER: 9
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
query($org: String!, $number: Int!) {
|
||||
organization(login: $org){
|
||||
projectNext(number: $number) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add Issue to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $id:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
@@ -23,6 +23,16 @@ services:
|
||||
ports:
|
||||
- "127.0.0.1:6379:6379"
|
||||
|
||||
minio:
|
||||
image: "minio/minio"
|
||||
command: server /data --console-address ":9001"
|
||||
restart: always
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
ports:
|
||||
- "127.0.0.1:9000:9000"
|
||||
- "127.0.0.1:9001:9001"
|
||||
|
||||
|
||||
# Useful for debugging / exploring local databases
|
||||
|
||||
@@ -57,3 +67,4 @@ volumes:
|
||||
redis-data:
|
||||
pgadmin-data:
|
||||
redis_insight-data:
|
||||
minio-data:
|
||||
|
||||
Generated
+17211
-12
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
commonjs: true,
|
||||
es2021: true
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 12
|
||||
},
|
||||
ignorePatterns: [ 'node_modules/*' ],
|
||||
extends: 'eslint:recommended',
|
||||
rules: {
|
||||
'object-curly-spacing': [ 'error', 'always' ],
|
||||
'array-bracket-spacing': [ 'error', 'always' ],
|
||||
'semi-spacing': [ 'error', { 'before': false, 'after': true } ],
|
||||
'space-in-parens': [ 'error', 'always' ],
|
||||
'space-before-blocks': 'error',
|
||||
'space-infix-ops': 'error',
|
||||
'comma-dangle': [ 'error', 'never' ],
|
||||
'no-console': [ 'warn', { allow: [ 'warn', 'error' ] } ],
|
||||
'space-unary-ops': 'error',
|
||||
'no-var': 'error',
|
||||
'no-alert': 'error',
|
||||
'no-param-reassign': 'warn',
|
||||
semi: [ 'error', 'never' ],
|
||||
quotes: [ 'error', 'single' ],
|
||||
eqeqeq: 'warn'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# `ifc-parser`
|
||||
|
||||
> TODO: description
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
const ifcParser = require('ifc-parser');
|
||||
|
||||
// TODO: DEMONSTRATE API
|
||||
```
|
||||
@@ -0,0 +1,153 @@
|
||||
'use strict'
|
||||
const crypto = require( 'crypto' )
|
||||
const crs = require( 'crypto-random-string' )
|
||||
const bcrypt = require( 'bcrypt' )
|
||||
|
||||
const knex = require( '../knex' )
|
||||
const Streams = ( ) => knex( 'streams' )
|
||||
const Branches = ( ) => knex( 'branches' )
|
||||
const Commits = ( ) => knex( 'commits' )
|
||||
const Objects = ( ) => knex( 'objects' )
|
||||
const Closures = ( ) => knex( 'object_children_closure' )
|
||||
const ApiTokens = ( ) => knex( 'api_tokens' )
|
||||
const TokenScopes = ( ) => knex( 'token_scopes' )
|
||||
|
||||
const StreamCommits = ( ) => knex( 'stream_commits' )
|
||||
const BranchCommits = ( ) => knex( 'branch_commits' )
|
||||
|
||||
module.exports = class ServerAPI {
|
||||
|
||||
constructor( { streamId } ) {
|
||||
this.streamId = streamId
|
||||
this.isSending = false
|
||||
this.buffer = []
|
||||
}
|
||||
|
||||
async saveObject( obj ) {
|
||||
if( !obj ) throw new Error( 'Null object' )
|
||||
|
||||
if( !obj.id ) {
|
||||
obj.id = crypto.createHash( 'md5' ).update( JSON.stringify( obj ) ).digest( 'hex' )
|
||||
}
|
||||
|
||||
await this.createObject( this.streamId, obj )
|
||||
|
||||
return obj.id
|
||||
}
|
||||
|
||||
async createObject( streamId, object ) {
|
||||
let insertionObject = this.prepInsertionObject( streamId, object )
|
||||
|
||||
let closures = [ ]
|
||||
let totalChildrenCountByDepth = {}
|
||||
if ( object.__closure !== null ) {
|
||||
for ( const prop in object.__closure ) {
|
||||
closures.push( { streamId: streamId, parent: insertionObject.id, child: prop, minDepth: object.__closure[ prop ] } )
|
||||
|
||||
if ( totalChildrenCountByDepth[ object.__closure[ prop ].toString( ) ] )
|
||||
totalChildrenCountByDepth[ object.__closure[ prop ].toString( ) ]++
|
||||
else
|
||||
totalChildrenCountByDepth[ object.__closure[ prop ].toString( ) ] = 1
|
||||
}
|
||||
}
|
||||
|
||||
delete insertionObject.__tree
|
||||
delete insertionObject.__closure
|
||||
|
||||
insertionObject.totalChildrenCount = closures.length
|
||||
insertionObject.totalChildrenCountByDepth = JSON.stringify( totalChildrenCountByDepth )
|
||||
|
||||
let q1 = Objects( ).insert( insertionObject ).toString( ) + ' on conflict do nothing'
|
||||
await knex.raw( q1 )
|
||||
|
||||
if ( closures.length > 0 ) {
|
||||
let q2 = `${ Closures().insert( closures ).toString() } on conflict do nothing`
|
||||
await knex.raw( q2 )
|
||||
}
|
||||
|
||||
return insertionObject.id
|
||||
}
|
||||
|
||||
prepInsertionObject( streamId, obj ) {
|
||||
const MAX_OBJECT_SIZE = 10 * 1024 * 1024
|
||||
|
||||
if ( obj.hash )
|
||||
obj.id = obj.hash
|
||||
else
|
||||
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 )
|
||||
if ( stringifiedObj.length > MAX_OBJECT_SIZE ) {
|
||||
throw new Error( `Object too large (${stringifiedObj.length} > ${MAX_OBJECT_SIZE})` )
|
||||
}
|
||||
|
||||
return {
|
||||
data: stringifiedObj, // stored in jsonb column
|
||||
streamId: streamId,
|
||||
id: obj.id,
|
||||
speckleType: obj.speckleType
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getBranchByNameAndStreamId( { streamId, name } ) {
|
||||
let query = Branches( ).select( '*' ).where( { streamId: streamId } ).andWhere( knex.raw( 'LOWER(name) = ?', [ name ] ) ).first( )
|
||||
return await query
|
||||
}
|
||||
|
||||
async createBranch( { name, description, streamId, authorId } ) {
|
||||
let branch = {}
|
||||
branch.id = crs( { length: 10 } )
|
||||
branch.streamId = streamId
|
||||
branch.authorId = authorId
|
||||
branch.name = name.toLowerCase( )
|
||||
branch.description = description
|
||||
|
||||
await Branches( ).returning( 'id' ).insert( branch )
|
||||
|
||||
// update stream updated at
|
||||
await Streams().where( { id: streamId } ).update( { updatedAt: knex.fn.now() } )
|
||||
|
||||
return branch.id
|
||||
}
|
||||
|
||||
async createBareToken( ) {
|
||||
let tokenId = crs( { length: 10 } )
|
||||
let tokenString = crs( { length: 32 } )
|
||||
let tokenHash = await bcrypt.hash( tokenString, 10 )
|
||||
let lastChars = tokenString.slice( tokenString.length - 6, tokenString.length )
|
||||
|
||||
return { tokenId, tokenString, tokenHash, lastChars }
|
||||
}
|
||||
|
||||
async createToken( { userId, name, scopes, lifespan } ) {
|
||||
let { tokenId, tokenString, tokenHash, lastChars } = await this.createBareToken( )
|
||||
|
||||
if ( scopes.length === 0 ) throw new Error( 'No scopes provided' )
|
||||
|
||||
let token = {
|
||||
id: tokenId,
|
||||
tokenDigest: tokenHash,
|
||||
lastChars: lastChars,
|
||||
owner: userId,
|
||||
name: name,
|
||||
lifespan: lifespan
|
||||
}
|
||||
let tokenScopes = scopes.map( scope => ( { tokenId: tokenId, scopeName: scope } ) )
|
||||
|
||||
await ApiTokens( ).insert( token )
|
||||
await TokenScopes( ).insert( tokenScopes )
|
||||
|
||||
return { id: tokenId, token: tokenId + tokenString }
|
||||
}
|
||||
|
||||
async revokeTokenById( tokenId ) {
|
||||
let delCount = await ApiTokens( ).where( { id: tokenId.slice( 0, 10 ) } ).del( )
|
||||
|
||||
if ( delCount === 0 )
|
||||
throw new Error( 'Token revokation failed' )
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,647 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ArchiCAD 9.00 Release 1 generated IFC file.','Build Number of the Ifc 2x3 interface: 63015 (11-04-2006)\X\0D\X\0A'),'2;1');
|
||||
FILE_NAME('C:\\Documents and Settings\\gkiss\\My Documents\\Ifc\\2x3\\MyPlans\\Railing.ifc','2006-05-26T11:14:01',('Architect'),('Building Designer Office'),'PreProc - EDM 4.5.0033','Windows System','The authorising person');
|
||||
FILE_SCHEMA(('IFC2X3'));
|
||||
ENDSEC;
|
||||
|
||||
DATA;
|
||||
#1= IFCORGANIZATION('GS','Graphisoft','Graphisoft',$,$);
|
||||
#5= IFCAPPLICATION(#1,'9.0','ArchiCAD 9.0','ArchiCAD');
|
||||
#6= IFCPERSON($,'Undefined',$,$,$,$,$,$);
|
||||
#8= IFCORGANIZATION($,'OrganizationName',$,$,$);
|
||||
#12= IFCPERSONANDORGANIZATION(#6,#8,$);
|
||||
#13= IFCOWNERHISTORY(#12,#5,$,.NOCHANGE.,$,$,$,1148634841);
|
||||
#14= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
|
||||
#15= IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#16= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#17= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
|
||||
#18= IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453293),#17);
|
||||
#19= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
|
||||
#20= IFCCONVERSIONBASEDUNIT(#19,.PLANEANGLEUNIT.,'DEGREE',#18);
|
||||
#21= IFCSIUNIT(*,.SOLIDANGLEUNIT.,$,.STERADIAN.);
|
||||
#22= IFCSIUNIT(*,.MASSUNIT.,$,.GRAM.);
|
||||
#23= IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
|
||||
#24= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.);
|
||||
#25= IFCSIUNIT(*,.LUMINOUSINTENSITYUNIT.,$,.LUMEN.);
|
||||
#26= IFCUNITASSIGNMENT((#14,#15,#16,#20,#21,#22,#23,#24,#25));
|
||||
#28= IFCDIRECTION((1.,0.,0.));
|
||||
#32= IFCDIRECTION((0.,1.,0.));
|
||||
#36= IFCDIRECTION((0.,0.,1.));
|
||||
#40= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#44= IFCAXIS2PLACEMENT3D(#40,#36,#28);
|
||||
#47= IFCDIRECTION((6.1230318E-17,1.));
|
||||
#51= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Design',3,1.0000000E-5,#44,#47);
|
||||
#54= IFCPROJECT('24a_voSxrA7gHUExjc3Mp6',#13,'Default Project',$,$,$,$,(#51,#1747),#26);
|
||||
#61= IFCLOCALPLACEMENT($,#44);
|
||||
#64= IFCBUILDING('0pppMBzUn8wfkj9sseUD8m',#13,'Default Building',$,$,#61,$,$,.ELEMENT.,$,$,$);
|
||||
#74= IFCAXIS2PLACEMENT3D(#40,#36,#28);
|
||||
#77= IFCLOCALPLACEMENT(#61,#74);
|
||||
#80= IFCBUILDINGSTOREY('3cAh0AqML3CwQE2GizlXBd',#13,'',$,$,#77,$,$,.ELEMENT.,0.);
|
||||
#90= IFCCARTESIANPOINT((-150.,25.,875.));
|
||||
#94= IFCCARTESIANPOINT((-150.,-25.,875.));
|
||||
#98= IFCCARTESIANPOINT((-150.,-25.,900.));
|
||||
#102= IFCCARTESIANPOINT((-150.,25.,900.));
|
||||
#106= IFCCARTESIANPOINT((1650.,25.,875.));
|
||||
#111= IFCCARTESIANPOINT((1650.,-25.,875.));
|
||||
#115= IFCCARTESIANPOINT((1650.,-25.,900.));
|
||||
#119= IFCCARTESIANPOINT((1650.,25.,900.));
|
||||
#123= IFCCARTESIANPOINT((-17.5,-17.5,0.));
|
||||
#127= IFCCARTESIANPOINT((17.5,-17.5,0.));
|
||||
#131= IFCCARTESIANPOINT((17.5,-17.5,875.));
|
||||
#135= IFCCARTESIANPOINT((-17.5,-17.5,875.));
|
||||
#139= IFCCARTESIANPOINT((-17.5,17.5,0.));
|
||||
#143= IFCCARTESIANPOINT((17.5,17.5,0.));
|
||||
#147= IFCCARTESIANPOINT((17.5,17.5,875.));
|
||||
#151= IFCCARTESIANPOINT((-17.5,17.5,875.));
|
||||
#155= IFCCARTESIANPOINT((732.5,-17.5,0.));
|
||||
#159= IFCCARTESIANPOINT((767.5,-17.5,0.));
|
||||
#163= IFCCARTESIANPOINT((767.5,-17.5,875.));
|
||||
#167= IFCCARTESIANPOINT((732.5,-17.5,875.));
|
||||
#171= IFCCARTESIANPOINT((732.5,17.5,0.));
|
||||
#175= IFCCARTESIANPOINT((767.5,17.5,0.));
|
||||
#179= IFCCARTESIANPOINT((767.5,17.5,875.));
|
||||
#183= IFCCARTESIANPOINT((732.5,17.5,875.));
|
||||
#187= IFCCARTESIANPOINT((1482.5,-17.5,0.));
|
||||
#191= IFCCARTESIANPOINT((1517.5,-17.5,0.));
|
||||
#195= IFCCARTESIANPOINT((1517.5,-17.5,875.));
|
||||
#199= IFCCARTESIANPOINT((1482.5,-17.5,875.));
|
||||
#203= IFCCARTESIANPOINT((1482.5,17.5,0.));
|
||||
#207= IFCCARTESIANPOINT((1517.5,17.5,0.));
|
||||
#211= IFCCARTESIANPOINT((1517.5,17.5,875.));
|
||||
#215= IFCCARTESIANPOINT((1482.5,17.5,875.));
|
||||
#219= IFCCARTESIANPOINT((732.5,-10.,170.));
|
||||
#223= IFCCARTESIANPOINT((732.5,10.,170.));
|
||||
#227= IFCCARTESIANPOINT((17.5,10.,170.));
|
||||
#231= IFCCARTESIANPOINT((17.5,-10.,170.));
|
||||
#235= IFCCARTESIANPOINT((732.5,10.,150.));
|
||||
#240= IFCCARTESIANPOINT((732.5,-10.,150.));
|
||||
#244= IFCCARTESIANPOINT((17.5,10.,150.));
|
||||
#248= IFCCARTESIANPOINT((17.5,-10.,150.));
|
||||
#252= IFCCARTESIANPOINT((1482.5,-10.,170.));
|
||||
#256= IFCCARTESIANPOINT((1482.5,10.,170.));
|
||||
#260= IFCCARTESIANPOINT((767.5,10.,170.));
|
||||
#264= IFCCARTESIANPOINT((767.5,-10.,170.));
|
||||
#268= IFCCARTESIANPOINT((1482.5,10.,150.));
|
||||
#272= IFCCARTESIANPOINT((1482.5,-10.,150.));
|
||||
#276= IFCCARTESIANPOINT((767.5,10.,150.));
|
||||
#280= IFCCARTESIANPOINT((767.5,-10.,150.));
|
||||
#284= IFCCARTESIANPOINT((144.5,-10.,170.));
|
||||
#288= IFCCARTESIANPOINT((164.5,-10.,170.));
|
||||
#292= IFCCARTESIANPOINT((164.5,-10.,875.));
|
||||
#296= IFCCARTESIANPOINT((144.5,-10.,875.));
|
||||
#300= IFCCARTESIANPOINT((144.5,10.,170.));
|
||||
#304= IFCCARTESIANPOINT((164.5,10.,170.));
|
||||
#308= IFCCARTESIANPOINT((164.5,10.,875.));
|
||||
#312= IFCCARTESIANPOINT((144.5,10.,875.));
|
||||
#316= IFCCARTESIANPOINT((291.5,-10.,170.));
|
||||
#320= IFCCARTESIANPOINT((311.5,-10.,170.));
|
||||
#324= IFCCARTESIANPOINT((311.5,-10.,875.));
|
||||
#328= IFCCARTESIANPOINT((291.5,-10.,875.));
|
||||
#332= IFCCARTESIANPOINT((291.5,10.,170.));
|
||||
#336= IFCCARTESIANPOINT((311.5,10.,170.));
|
||||
#340= IFCCARTESIANPOINT((311.5,10.,875.));
|
||||
#344= IFCCARTESIANPOINT((291.5,10.,875.));
|
||||
#348= IFCCARTESIANPOINT((438.5,-10.,170.));
|
||||
#352= IFCCARTESIANPOINT((458.5,-10.,170.));
|
||||
#356= IFCCARTESIANPOINT((458.5,-10.,875.));
|
||||
#360= IFCCARTESIANPOINT((438.5,-10.,875.));
|
||||
#364= IFCCARTESIANPOINT((438.5,10.,170.));
|
||||
#369= IFCCARTESIANPOINT((458.5,10.,170.));
|
||||
#373= IFCCARTESIANPOINT((458.5,10.,875.));
|
||||
#377= IFCCARTESIANPOINT((438.5,10.,875.));
|
||||
#381= IFCCARTESIANPOINT((585.5,-10.,170.));
|
||||
#385= IFCCARTESIANPOINT((605.5,-10.,170.));
|
||||
#389= IFCCARTESIANPOINT((605.5,-10.,875.));
|
||||
#393= IFCCARTESIANPOINT((585.5,-10.,875.));
|
||||
#397= IFCCARTESIANPOINT((585.5,10.,170.));
|
||||
#401= IFCCARTESIANPOINT((605.5,10.,170.));
|
||||
#405= IFCCARTESIANPOINT((605.5,10.,875.));
|
||||
#409= IFCCARTESIANPOINT((585.5,10.,875.));
|
||||
#413= IFCCARTESIANPOINT((894.5,-10.,170.));
|
||||
#417= IFCCARTESIANPOINT((914.5,-10.,170.));
|
||||
#421= IFCCARTESIANPOINT((914.5,-10.,875.));
|
||||
#425= IFCCARTESIANPOINT((894.5,-10.,875.));
|
||||
#429= IFCCARTESIANPOINT((894.5,10.,170.));
|
||||
#433= IFCCARTESIANPOINT((914.5,10.,170.));
|
||||
#437= IFCCARTESIANPOINT((914.5,10.,875.));
|
||||
#441= IFCCARTESIANPOINT((894.5,10.,875.));
|
||||
#445= IFCCARTESIANPOINT((1041.5,-10.,170.));
|
||||
#449= IFCCARTESIANPOINT((1061.5,-10.,170.));
|
||||
#453= IFCCARTESIANPOINT((1061.5,-10.,875.));
|
||||
#457= IFCCARTESIANPOINT((1041.5,-10.,875.));
|
||||
#461= IFCCARTESIANPOINT((1041.5,10.,170.));
|
||||
#465= IFCCARTESIANPOINT((1061.5,10.,170.));
|
||||
#469= IFCCARTESIANPOINT((1061.5,10.,875.));
|
||||
#473= IFCCARTESIANPOINT((1041.5,10.,875.));
|
||||
#477= IFCCARTESIANPOINT((1188.5,-10.,170.));
|
||||
#481= IFCCARTESIANPOINT((1208.5,-10.,170.));
|
||||
#485= IFCCARTESIANPOINT((1208.5,-10.,875.));
|
||||
#489= IFCCARTESIANPOINT((1188.5,-10.,875.));
|
||||
#494= IFCCARTESIANPOINT((1188.5,10.,170.));
|
||||
#498= IFCCARTESIANPOINT((1208.5,10.,170.));
|
||||
#502= IFCCARTESIANPOINT((1208.5,10.,875.));
|
||||
#506= IFCCARTESIANPOINT((1188.5,10.,875.));
|
||||
#510= IFCCARTESIANPOINT((1335.5,-10.,170.));
|
||||
#514= IFCCARTESIANPOINT((1355.5,-10.,170.));
|
||||
#518= IFCCARTESIANPOINT((1355.5,-10.,875.));
|
||||
#522= IFCCARTESIANPOINT((1335.5,-10.,875.));
|
||||
#526= IFCCARTESIANPOINT((1335.5,10.,170.));
|
||||
#530= IFCCARTESIANPOINT((1355.5,10.,170.));
|
||||
#534= IFCCARTESIANPOINT((1355.5,10.,875.));
|
||||
#538= IFCCARTESIANPOINT((1335.5,10.,875.));
|
||||
#542= IFCPOLYLOOP((#90,#94,#98,#102));
|
||||
#546= IFCFACEOUTERBOUND(#542,.T.);
|
||||
#549= IFCFACE((#546));
|
||||
#553= IFCPOLYLOOP((#90,#106,#111,#94));
|
||||
#557= IFCFACEOUTERBOUND(#553,.T.);
|
||||
#560= IFCFACE((#557));
|
||||
#564= IFCPOLYLOOP((#94,#111,#115,#98));
|
||||
#568= IFCFACEOUTERBOUND(#564,.T.);
|
||||
#571= IFCFACE((#568));
|
||||
#575= IFCPOLYLOOP((#102,#98,#115,#119));
|
||||
#579= IFCFACEOUTERBOUND(#575,.T.);
|
||||
#582= IFCFACE((#579));
|
||||
#586= IFCPOLYLOOP((#106,#90,#102,#119));
|
||||
#590= IFCFACEOUTERBOUND(#586,.T.);
|
||||
#593= IFCFACE((#590));
|
||||
#597= IFCPOLYLOOP((#111,#106,#119,#115));
|
||||
#601= IFCFACEOUTERBOUND(#597,.T.);
|
||||
#604= IFCFACE((#601));
|
||||
#608= IFCCLOSEDSHELL((#549,#560,#571,#582,#593,#604));
|
||||
#612= IFCFACETEDBREP(#608);
|
||||
#615= IFCCOLOURRGB($,0.88227665,0.40103761,0.22470436);
|
||||
#616= IFCSURFACESTYLERENDERING(#615,0.,IFCNORMALISEDRATIOMEASURE(0.),$,$,$,IFCNORMALISEDRATIOMEASURE(0.),$,.NOTDEFINED.);
|
||||
#617= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#619= IFCPRESENTATIONSTYLEASSIGNMENT((#617));
|
||||
#622= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#624= IFCPRESENTATIONSTYLEASSIGNMENT((#622));
|
||||
#626= IFCSTYLEDITEM(#612,(#624),$);
|
||||
#630= IFCPOLYLOOP((#123,#127,#131,#135));
|
||||
#634= IFCFACEOUTERBOUND(#630,.T.);
|
||||
#637= IFCFACE((#634));
|
||||
#641= IFCPOLYLOOP((#123,#139,#143,#127));
|
||||
#645= IFCFACEOUTERBOUND(#641,.T.);
|
||||
#648= IFCFACE((#645));
|
||||
#652= IFCPOLYLOOP((#127,#143,#147,#131));
|
||||
#656= IFCFACEOUTERBOUND(#652,.T.);
|
||||
#659= IFCFACE((#656));
|
||||
#663= IFCPOLYLOOP((#135,#131,#147,#151));
|
||||
#667= IFCFACEOUTERBOUND(#663,.T.);
|
||||
#670= IFCFACE((#667));
|
||||
#674= IFCPOLYLOOP((#139,#123,#135,#151));
|
||||
#678= IFCFACEOUTERBOUND(#674,.T.);
|
||||
#681= IFCFACE((#678));
|
||||
#685= IFCPOLYLOOP((#143,#139,#151,#147));
|
||||
#689= IFCFACEOUTERBOUND(#685,.T.);
|
||||
#692= IFCFACE((#689));
|
||||
#696= IFCCLOSEDSHELL((#637,#648,#659,#670,#681,#692));
|
||||
#700= IFCFACETEDBREP(#696);
|
||||
#703= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#705= IFCPRESENTATIONSTYLEASSIGNMENT((#703));
|
||||
#707= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#709= IFCPRESENTATIONSTYLEASSIGNMENT((#707));
|
||||
#711= IFCSTYLEDITEM(#700,(#709),$);
|
||||
#715= IFCPOLYLOOP((#155,#159,#163,#167));
|
||||
#719= IFCFACEOUTERBOUND(#715,.T.);
|
||||
#722= IFCFACE((#719));
|
||||
#726= IFCPOLYLOOP((#155,#171,#175,#159));
|
||||
#730= IFCFACEOUTERBOUND(#726,.T.);
|
||||
#733= IFCFACE((#730));
|
||||
#737= IFCPOLYLOOP((#159,#175,#179,#163));
|
||||
#741= IFCFACEOUTERBOUND(#737,.T.);
|
||||
#744= IFCFACE((#741));
|
||||
#748= IFCPOLYLOOP((#167,#163,#179,#183));
|
||||
#753= IFCFACEOUTERBOUND(#748,.T.);
|
||||
#756= IFCFACE((#753));
|
||||
#760= IFCPOLYLOOP((#171,#155,#167,#183));
|
||||
#764= IFCFACEOUTERBOUND(#760,.T.);
|
||||
#767= IFCFACE((#764));
|
||||
#771= IFCPOLYLOOP((#175,#171,#183,#179));
|
||||
#775= IFCFACEOUTERBOUND(#771,.T.);
|
||||
#778= IFCFACE((#775));
|
||||
#782= IFCCLOSEDSHELL((#722,#733,#744,#756,#767,#778));
|
||||
#786= IFCFACETEDBREP(#782);
|
||||
#789= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#791= IFCPRESENTATIONSTYLEASSIGNMENT((#789));
|
||||
#793= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#795= IFCPRESENTATIONSTYLEASSIGNMENT((#793));
|
||||
#797= IFCSTYLEDITEM(#786,(#795),$);
|
||||
#801= IFCPOLYLOOP((#187,#191,#195,#199));
|
||||
#805= IFCFACEOUTERBOUND(#801,.T.);
|
||||
#808= IFCFACE((#805));
|
||||
#812= IFCPOLYLOOP((#187,#203,#207,#191));
|
||||
#816= IFCFACEOUTERBOUND(#812,.T.);
|
||||
#819= IFCFACE((#816));
|
||||
#823= IFCPOLYLOOP((#191,#207,#211,#195));
|
||||
#827= IFCFACEOUTERBOUND(#823,.T.);
|
||||
#830= IFCFACE((#827));
|
||||
#834= IFCPOLYLOOP((#199,#195,#211,#215));
|
||||
#838= IFCFACEOUTERBOUND(#834,.T.);
|
||||
#841= IFCFACE((#838));
|
||||
#845= IFCPOLYLOOP((#203,#187,#199,#215));
|
||||
#849= IFCFACEOUTERBOUND(#845,.T.);
|
||||
#852= IFCFACE((#849));
|
||||
#856= IFCPOLYLOOP((#207,#203,#215,#211));
|
||||
#860= IFCFACEOUTERBOUND(#856,.T.);
|
||||
#863= IFCFACE((#860));
|
||||
#867= IFCCLOSEDSHELL((#808,#819,#830,#841,#852,#863));
|
||||
#871= IFCFACETEDBREP(#867);
|
||||
#874= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#876= IFCPRESENTATIONSTYLEASSIGNMENT((#874));
|
||||
#879= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#881= IFCPRESENTATIONSTYLEASSIGNMENT((#879));
|
||||
#883= IFCSTYLEDITEM(#871,(#881),$);
|
||||
#887= IFCPOLYLOOP((#219,#223,#227,#231));
|
||||
#891= IFCFACEOUTERBOUND(#887,.T.);
|
||||
#894= IFCFACE((#891));
|
||||
#898= IFCPOLYLOOP((#235,#223,#219,#240));
|
||||
#902= IFCFACEOUTERBOUND(#898,.T.);
|
||||
#905= IFCFACE((#902));
|
||||
#909= IFCPOLYLOOP((#223,#235,#244,#227));
|
||||
#913= IFCFACEOUTERBOUND(#909,.T.);
|
||||
#916= IFCFACE((#913));
|
||||
#920= IFCPOLYLOOP((#248,#231,#227,#244));
|
||||
#924= IFCFACEOUTERBOUND(#920,.T.);
|
||||
#927= IFCFACE((#924));
|
||||
#931= IFCPOLYLOOP((#240,#219,#231,#248));
|
||||
#935= IFCFACEOUTERBOUND(#931,.T.);
|
||||
#938= IFCFACE((#935));
|
||||
#942= IFCPOLYLOOP((#235,#240,#248,#244));
|
||||
#946= IFCFACEOUTERBOUND(#942,.T.);
|
||||
#949= IFCFACE((#946));
|
||||
#953= IFCCLOSEDSHELL((#894,#905,#916,#927,#938,#949));
|
||||
#957= IFCFACETEDBREP(#953);
|
||||
#960= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#962= IFCPRESENTATIONSTYLEASSIGNMENT((#960));
|
||||
#964= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#966= IFCPRESENTATIONSTYLEASSIGNMENT((#964));
|
||||
#968= IFCSTYLEDITEM(#957,(#966),$);
|
||||
#972= IFCPOLYLOOP((#252,#256,#260,#264));
|
||||
#976= IFCFACEOUTERBOUND(#972,.T.);
|
||||
#979= IFCFACE((#976));
|
||||
#983= IFCPOLYLOOP((#268,#256,#252,#272));
|
||||
#987= IFCFACEOUTERBOUND(#983,.T.);
|
||||
#990= IFCFACE((#987));
|
||||
#994= IFCPOLYLOOP((#256,#268,#276,#260));
|
||||
#998= IFCFACEOUTERBOUND(#994,.T.);
|
||||
#1001= IFCFACE((#998));
|
||||
#1006= IFCPOLYLOOP((#280,#264,#260,#276));
|
||||
#1010= IFCFACEOUTERBOUND(#1006,.T.);
|
||||
#1013= IFCFACE((#1010));
|
||||
#1017= IFCPOLYLOOP((#272,#252,#264,#280));
|
||||
#1021= IFCFACEOUTERBOUND(#1017,.T.);
|
||||
#1024= IFCFACE((#1021));
|
||||
#1028= IFCPOLYLOOP((#268,#272,#280,#276));
|
||||
#1032= IFCFACEOUTERBOUND(#1028,.T.);
|
||||
#1035= IFCFACE((#1032));
|
||||
#1039= IFCCLOSEDSHELL((#979,#990,#1001,#1013,#1024,#1035));
|
||||
#1043= IFCFACETEDBREP(#1039);
|
||||
#1046= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1048= IFCPRESENTATIONSTYLEASSIGNMENT((#1046));
|
||||
#1050= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1052= IFCPRESENTATIONSTYLEASSIGNMENT((#1050));
|
||||
#1054= IFCSTYLEDITEM(#1043,(#1052),$);
|
||||
#1058= IFCPOLYLOOP((#284,#288,#292,#296));
|
||||
#1062= IFCFACEOUTERBOUND(#1058,.T.);
|
||||
#1065= IFCFACE((#1062));
|
||||
#1069= IFCPOLYLOOP((#284,#300,#304,#288));
|
||||
#1073= IFCFACEOUTERBOUND(#1069,.T.);
|
||||
#1076= IFCFACE((#1073));
|
||||
#1080= IFCPOLYLOOP((#288,#304,#308,#292));
|
||||
#1084= IFCFACEOUTERBOUND(#1080,.T.);
|
||||
#1087= IFCFACE((#1084));
|
||||
#1091= IFCPOLYLOOP((#296,#292,#308,#312));
|
||||
#1095= IFCFACEOUTERBOUND(#1091,.T.);
|
||||
#1098= IFCFACE((#1095));
|
||||
#1102= IFCPOLYLOOP((#300,#284,#296,#312));
|
||||
#1106= IFCFACEOUTERBOUND(#1102,.T.);
|
||||
#1109= IFCFACE((#1106));
|
||||
#1113= IFCPOLYLOOP((#304,#300,#312,#308));
|
||||
#1117= IFCFACEOUTERBOUND(#1113,.T.);
|
||||
#1120= IFCFACE((#1117));
|
||||
#1124= IFCCLOSEDSHELL((#1065,#1076,#1087,#1098,#1109,#1120));
|
||||
#1128= IFCFACETEDBREP(#1124);
|
||||
#1131= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1134= IFCPRESENTATIONSTYLEASSIGNMENT((#1131));
|
||||
#1136= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1138= IFCPRESENTATIONSTYLEASSIGNMENT((#1136));
|
||||
#1140= IFCSTYLEDITEM(#1128,(#1138),$);
|
||||
#1144= IFCPOLYLOOP((#316,#320,#324,#328));
|
||||
#1148= IFCFACEOUTERBOUND(#1144,.T.);
|
||||
#1151= IFCFACE((#1148));
|
||||
#1155= IFCPOLYLOOP((#316,#332,#336,#320));
|
||||
#1159= IFCFACEOUTERBOUND(#1155,.T.);
|
||||
#1162= IFCFACE((#1159));
|
||||
#1166= IFCPOLYLOOP((#320,#336,#340,#324));
|
||||
#1170= IFCFACEOUTERBOUND(#1166,.T.);
|
||||
#1173= IFCFACE((#1170));
|
||||
#1177= IFCPOLYLOOP((#328,#324,#340,#344));
|
||||
#1181= IFCFACEOUTERBOUND(#1177,.T.);
|
||||
#1184= IFCFACE((#1181));
|
||||
#1188= IFCPOLYLOOP((#332,#316,#328,#344));
|
||||
#1192= IFCFACEOUTERBOUND(#1188,.T.);
|
||||
#1195= IFCFACE((#1192));
|
||||
#1199= IFCPOLYLOOP((#336,#332,#344,#340));
|
||||
#1203= IFCFACEOUTERBOUND(#1199,.T.);
|
||||
#1206= IFCFACE((#1203));
|
||||
#1210= IFCCLOSEDSHELL((#1151,#1162,#1173,#1184,#1195,#1206));
|
||||
#1214= IFCFACETEDBREP(#1210);
|
||||
#1217= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1219= IFCPRESENTATIONSTYLEASSIGNMENT((#1217));
|
||||
#1221= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1223= IFCPRESENTATIONSTYLEASSIGNMENT((#1221));
|
||||
#1225= IFCSTYLEDITEM(#1214,(#1223),$);
|
||||
#1229= IFCPOLYLOOP((#348,#352,#356,#360));
|
||||
#1233= IFCFACEOUTERBOUND(#1229,.T.);
|
||||
#1236= IFCFACE((#1233));
|
||||
#1240= IFCPOLYLOOP((#348,#364,#369,#352));
|
||||
#1244= IFCFACEOUTERBOUND(#1240,.T.);
|
||||
#1247= IFCFACE((#1244));
|
||||
#1251= IFCPOLYLOOP((#352,#369,#373,#356));
|
||||
#1255= IFCFACEOUTERBOUND(#1251,.T.);
|
||||
#1258= IFCFACE((#1255));
|
||||
#1263= IFCPOLYLOOP((#360,#356,#373,#377));
|
||||
#1267= IFCFACEOUTERBOUND(#1263,.T.);
|
||||
#1270= IFCFACE((#1267));
|
||||
#1274= IFCPOLYLOOP((#364,#348,#360,#377));
|
||||
#1278= IFCFACEOUTERBOUND(#1274,.T.);
|
||||
#1281= IFCFACE((#1278));
|
||||
#1285= IFCPOLYLOOP((#369,#364,#377,#373));
|
||||
#1289= IFCFACEOUTERBOUND(#1285,.T.);
|
||||
#1292= IFCFACE((#1289));
|
||||
#1296= IFCCLOSEDSHELL((#1236,#1247,#1258,#1270,#1281,#1292));
|
||||
#1300= IFCFACETEDBREP(#1296);
|
||||
#1303= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1305= IFCPRESENTATIONSTYLEASSIGNMENT((#1303));
|
||||
#1307= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1309= IFCPRESENTATIONSTYLEASSIGNMENT((#1307));
|
||||
#1311= IFCSTYLEDITEM(#1300,(#1309),$);
|
||||
#1315= IFCPOLYLOOP((#381,#385,#389,#393));
|
||||
#1319= IFCFACEOUTERBOUND(#1315,.T.);
|
||||
#1322= IFCFACE((#1319));
|
||||
#1326= IFCPOLYLOOP((#381,#397,#401,#385));
|
||||
#1330= IFCFACEOUTERBOUND(#1326,.T.);
|
||||
#1333= IFCFACE((#1330));
|
||||
#1337= IFCPOLYLOOP((#385,#401,#405,#389));
|
||||
#1341= IFCFACEOUTERBOUND(#1337,.T.);
|
||||
#1344= IFCFACE((#1341));
|
||||
#1348= IFCPOLYLOOP((#393,#389,#405,#409));
|
||||
#1352= IFCFACEOUTERBOUND(#1348,.T.);
|
||||
#1355= IFCFACE((#1352));
|
||||
#1359= IFCPOLYLOOP((#397,#381,#393,#409));
|
||||
#1363= IFCFACEOUTERBOUND(#1359,.T.);
|
||||
#1366= IFCFACE((#1363));
|
||||
#1370= IFCPOLYLOOP((#401,#397,#409,#405));
|
||||
#1374= IFCFACEOUTERBOUND(#1370,.T.);
|
||||
#1377= IFCFACE((#1374));
|
||||
#1381= IFCCLOSEDSHELL((#1322,#1333,#1344,#1355,#1366,#1377));
|
||||
#1385= IFCFACETEDBREP(#1381);
|
||||
#1388= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1391= IFCPRESENTATIONSTYLEASSIGNMENT((#1388));
|
||||
#1393= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1395= IFCPRESENTATIONSTYLEASSIGNMENT((#1393));
|
||||
#1397= IFCSTYLEDITEM(#1385,(#1395),$);
|
||||
#1401= IFCPOLYLOOP((#413,#417,#421,#425));
|
||||
#1405= IFCFACEOUTERBOUND(#1401,.T.);
|
||||
#1408= IFCFACE((#1405));
|
||||
#1412= IFCPOLYLOOP((#413,#429,#433,#417));
|
||||
#1416= IFCFACEOUTERBOUND(#1412,.T.);
|
||||
#1419= IFCFACE((#1416));
|
||||
#1423= IFCPOLYLOOP((#417,#433,#437,#421));
|
||||
#1427= IFCFACEOUTERBOUND(#1423,.T.);
|
||||
#1430= IFCFACE((#1427));
|
||||
#1434= IFCPOLYLOOP((#425,#421,#437,#441));
|
||||
#1438= IFCFACEOUTERBOUND(#1434,.T.);
|
||||
#1441= IFCFACE((#1438));
|
||||
#1445= IFCPOLYLOOP((#429,#413,#425,#441));
|
||||
#1449= IFCFACEOUTERBOUND(#1445,.T.);
|
||||
#1452= IFCFACE((#1449));
|
||||
#1456= IFCPOLYLOOP((#433,#429,#441,#437));
|
||||
#1460= IFCFACEOUTERBOUND(#1456,.T.);
|
||||
#1463= IFCFACE((#1460));
|
||||
#1467= IFCCLOSEDSHELL((#1408,#1419,#1430,#1441,#1452,#1463));
|
||||
#1471= IFCFACETEDBREP(#1467);
|
||||
#1474= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1476= IFCPRESENTATIONSTYLEASSIGNMENT((#1474));
|
||||
#1478= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1480= IFCPRESENTATIONSTYLEASSIGNMENT((#1478));
|
||||
#1482= IFCSTYLEDITEM(#1471,(#1480),$);
|
||||
#1486= IFCPOLYLOOP((#445,#449,#453,#457));
|
||||
#1490= IFCFACEOUTERBOUND(#1486,.T.);
|
||||
#1493= IFCFACE((#1490));
|
||||
#1497= IFCPOLYLOOP((#445,#461,#465,#449));
|
||||
#1501= IFCFACEOUTERBOUND(#1497,.T.);
|
||||
#1504= IFCFACE((#1501));
|
||||
#1508= IFCPOLYLOOP((#449,#465,#469,#453));
|
||||
#1512= IFCFACEOUTERBOUND(#1508,.T.);
|
||||
#1515= IFCFACE((#1512));
|
||||
#1520= IFCPOLYLOOP((#457,#453,#469,#473));
|
||||
#1524= IFCFACEOUTERBOUND(#1520,.T.);
|
||||
#1527= IFCFACE((#1524));
|
||||
#1531= IFCPOLYLOOP((#461,#445,#457,#473));
|
||||
#1535= IFCFACEOUTERBOUND(#1531,.T.);
|
||||
#1538= IFCFACE((#1535));
|
||||
#1542= IFCPOLYLOOP((#465,#461,#473,#469));
|
||||
#1546= IFCFACEOUTERBOUND(#1542,.T.);
|
||||
#1549= IFCFACE((#1546));
|
||||
#1553= IFCCLOSEDSHELL((#1493,#1504,#1515,#1527,#1538,#1549));
|
||||
#1557= IFCFACETEDBREP(#1553);
|
||||
#1560= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1562= IFCPRESENTATIONSTYLEASSIGNMENT((#1560));
|
||||
#1564= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1566= IFCPRESENTATIONSTYLEASSIGNMENT((#1564));
|
||||
#1568= IFCSTYLEDITEM(#1557,(#1566),$);
|
||||
#1572= IFCPOLYLOOP((#477,#481,#485,#489));
|
||||
#1576= IFCFACEOUTERBOUND(#1572,.T.);
|
||||
#1579= IFCFACE((#1576));
|
||||
#1583= IFCPOLYLOOP((#477,#494,#498,#481));
|
||||
#1587= IFCFACEOUTERBOUND(#1583,.T.);
|
||||
#1590= IFCFACE((#1587));
|
||||
#1594= IFCPOLYLOOP((#481,#498,#502,#485));
|
||||
#1598= IFCFACEOUTERBOUND(#1594,.T.);
|
||||
#1601= IFCFACE((#1598));
|
||||
#1605= IFCPOLYLOOP((#489,#485,#502,#506));
|
||||
#1609= IFCFACEOUTERBOUND(#1605,.T.);
|
||||
#1612= IFCFACE((#1609));
|
||||
#1616= IFCPOLYLOOP((#494,#477,#489,#506));
|
||||
#1620= IFCFACEOUTERBOUND(#1616,.T.);
|
||||
#1623= IFCFACE((#1620));
|
||||
#1627= IFCPOLYLOOP((#498,#494,#506,#502));
|
||||
#1631= IFCFACEOUTERBOUND(#1627,.T.);
|
||||
#1634= IFCFACE((#1631));
|
||||
#1638= IFCCLOSEDSHELL((#1579,#1590,#1601,#1612,#1623,#1634));
|
||||
#1642= IFCFACETEDBREP(#1638);
|
||||
#1646= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1648= IFCPRESENTATIONSTYLEASSIGNMENT((#1646));
|
||||
#1650= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1652= IFCPRESENTATIONSTYLEASSIGNMENT((#1650));
|
||||
#1654= IFCSTYLEDITEM(#1642,(#1652),$);
|
||||
#1658= IFCPOLYLOOP((#510,#514,#518,#522));
|
||||
#1662= IFCFACEOUTERBOUND(#1658,.T.);
|
||||
#1665= IFCFACE((#1662));
|
||||
#1669= IFCPOLYLOOP((#510,#526,#530,#514));
|
||||
#1673= IFCFACEOUTERBOUND(#1669,.T.);
|
||||
#1676= IFCFACE((#1673));
|
||||
#1680= IFCPOLYLOOP((#514,#530,#534,#518));
|
||||
#1684= IFCFACEOUTERBOUND(#1680,.T.);
|
||||
#1687= IFCFACE((#1684));
|
||||
#1691= IFCPOLYLOOP((#522,#518,#534,#538));
|
||||
#1695= IFCFACEOUTERBOUND(#1691,.T.);
|
||||
#1698= IFCFACE((#1695));
|
||||
#1702= IFCPOLYLOOP((#526,#510,#522,#538));
|
||||
#1706= IFCFACEOUTERBOUND(#1702,.T.);
|
||||
#1709= IFCFACE((#1706));
|
||||
#1713= IFCPOLYLOOP((#530,#526,#538,#534));
|
||||
#1717= IFCFACEOUTERBOUND(#1713,.T.);
|
||||
#1720= IFCFACE((#1717));
|
||||
#1724= IFCCLOSEDSHELL((#1665,#1676,#1687,#1698,#1709,#1720));
|
||||
#1728= IFCFACETEDBREP(#1724);
|
||||
#1731= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1733= IFCPRESENTATIONSTYLEASSIGNMENT((#1731));
|
||||
#1735= IFCSURFACESTYLE($,.BOTH.,(#616));
|
||||
#1737= IFCPRESENTATIONSTYLEASSIGNMENT((#1735));
|
||||
#1739= IFCSTYLEDITEM(#1728,(#1737),$);
|
||||
#1743= IFCDIRECTION((6.1230318E-17,1.));
|
||||
#1747= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Model',3,1.0000000E-5,#44,#1743);
|
||||
#1751= IFCSHAPEREPRESENTATION(#1747,'Body','Brep',(#612,#700,#786,#871,#957,#1043,#1128,#1214,#1300,#1385,#1471,#1557,#1642,#1728));
|
||||
#1757= IFCPRODUCTDEFINITIONSHAPE($,$,(#1751));
|
||||
#1761= IFCCARTESIANPOINT((203.55989,128.43353,0.));
|
||||
#1765= IFCAXIS2PLACEMENT3D(#1761,#36,#28);
|
||||
#1768= IFCLOCALPLACEMENT(#77,#1765);
|
||||
#1771= IFCRAILING('0zUE$sW7XAm9ihLEhV0q1w',#13,'',$,$,#1768,#1757,$,.HANDRAIL.);
|
||||
#1791= IFCPROPERTYSINGLEVALUE('ArchiCAD_UID',$,IFCINTEGER(235),$);
|
||||
#1795= IFCCOMPLEXPROPERTY('OBJECT',$,'ArchiCAD',(#1791));
|
||||
#1800= IFCPROPERTYSET('3KvQZMWnbAZeaaPcRQxbJ7',#13,'Graphisoft AC90 OBJECT','Graphisoft AC90',(#1795,#2119));
|
||||
#1805= IFCPROPERTYSINGLEVALUE('API_LIBNAME',$,IFCLABEL('Rail Post with Balusters'),$);
|
||||
#1809= IFCPROPERTYSINGLEVALUE('A',$,IFCNUMERICMEASURE(1.8),$);
|
||||
#1813= IFCPROPERTYSINGLEVALUE('B',$,IFCNUMERICMEASURE(0.05),$);
|
||||
#1817= IFCPROPERTYSINGLEVALUE('ZZYZX',$,IFCNUMERICMEASURE(0.9),$);
|
||||
#1821= IFCPROPERTYSINGLEVALUE('gs_detlevel_3D',$,IFCLABEL('Detailed'),$);
|
||||
#1825= IFCPROPERTYSINGLEVALUE('cfs',$,IFCNUMERICMEASURE(0.),$);
|
||||
#1829= IFCPROPERTYSINGLEVALUE('ptyp',$,IFCLABEL('Straight'),$);
|
||||
#1833= IFCPROPERTYSINGLEVALUE('ani',$,IFCNUMERICMEASURE(0.),$);
|
||||
#1837= IFCPROPERTYSINGLEVALUE('rom',$,IFCLABEL('Vertical'),$);
|
||||
#1841= IFCPROPERTYSINGLEVALUE('rmm',$,IFCLABEL('Vertical'),$);
|
||||
#1845= IFCPROPERTYSINGLEVALUE('LengthSize',$,IFCNUMERICMEASURE(0.),$);
|
||||
#1849= IFCPROPERTYSINGLEVALUE('lra',$,IFCNUMERICMEASURE(1.5),$);
|
||||
#1853= IFCPROPERTYSINGLEVALUE('lrah',$,IFCNUMERICMEASURE(1.5),$);
|
||||
#1857= IFCPROPERTYSINGLEVALUE('hr',$,IFCNUMERICMEASURE(0.75),$);
|
||||
#1861= IFCPROPERTYSINGLEVALUE('hrPerp',$,IFCNUMERICMEASURE(0.75),$);
|
||||
#1865= IFCPROPERTYSINGLEVALUE('ab',$,IFCNUMERICMEASURE(1.5707963),$);
|
||||
#1869= IFCPROPERTYSINGLEVALUE('rb',$,IFCNUMERICMEASURE(1.),$);
|
||||
#1873= IFCPROPERTYSINGLEVALUE('ds',$,IFCNUMERICMEASURE(0.15),$);
|
||||
#1877= IFCPROPERTYSINGLEVALUE('dsPerp',$,IFCNUMERICMEASURE(0.15),$);
|
||||
#1881= IFCPROPERTYSINGLEVALUE('gs_AngleL',$,IFCNUMERICMEASURE(90.),$);
|
||||
#1885= IFCPROPERTYSINGLEVALUE('gs_AngleR',$,IFCNUMERICMEASURE(90.),$);
|
||||
#1889= IFCPROPERTYSINGLEVALUE('hrt',$,IFCLABEL('Rectangular'),$);
|
||||
#1893= IFCPROPERTYSINGLEVALUE('hrd',$,IFCNUMERICMEASURE(0.05),$);
|
||||
#1897= IFCPROPERTYSINGLEVALUE('hrw',$,IFCNUMERICMEASURE(0.05),$);
|
||||
#1902= IFCPROPERTYSINGLEVALUE('hrh',$,IFCNUMERICMEASURE(0.025),$);
|
||||
#1906= IFCPROPERTYSINGLEVALUE('hro',$,IFCNUMERICMEASURE(0.),$);
|
||||
#1910= IFCPROPERTYSINGLEVALUE('hroL',$,IFCNUMERICMEASURE(0.15),$);
|
||||
#1914= IFCPROPERTYSINGLEVALUE('hroR',$,IFCNUMERICMEASURE(0.15),$);
|
||||
#1918= IFCPROPERTYSINGLEVALUE('hroLDiagonal',$,IFCNUMERICMEASURE(0.15),$);
|
||||
#1922= IFCPROPERTYSINGLEVALUE('hroRDiagonal',$,IFCNUMERICMEASURE(0.15),$);
|
||||
#1926= IFCPROPERTYSINGLEVALUE('pt',$,IFCLABEL('Rectangular'),$);
|
||||
#1930= IFCPROPERTYSINGLEVALUE('np',$,IFCNUMERICMEASURE(3.),$);
|
||||
#1934= IFCPROPERTYSINGLEVALUE('pd',$,IFCNUMERICMEASURE(0.035),$);
|
||||
#1938= IFCPROPERTYSINGLEVALUE('pw',$,IFCNUMERICMEASURE(0.035),$);
|
||||
#1942= IFCPROPERTYSINGLEVALUE('pth',$,IFCNUMERICMEASURE(0.035),$);
|
||||
#1946= IFCPROPERTYSINGLEVALUE('gs_PostBaseOverhang',$,IFCNUMERICMEASURE(0.),$);
|
||||
#1950= IFCPROPERTYSINGLEVALUE('gs_PostBaseOverhangPerp',$,IFCNUMERICMEASURE(0.),$);
|
||||
#1954= IFCPROPERTYSINGLEVALUE('bt',$,IFCLABEL('Rectangular'),$);
|
||||
#1958= IFCPROPERTYSINGLEVALUE('nb',$,IFCNUMERICMEASURE(4.),$);
|
||||
#1962= IFCPROPERTYSINGLEVALUE('bd',$,IFCNUMERICMEASURE(0.02),$);
|
||||
#1966= IFCPROPERTYSINGLEVALUE('bth',$,IFCNUMERICMEASURE(0.02),$);
|
||||
#1970= IFCPROPERTYSINGLEVALUE('bw',$,IFCNUMERICMEASURE(0.02),$);
|
||||
#1974= IFCPROPERTYSINGLEVALUE('res',$,IFCNUMERICMEASURE(32.),$);
|
||||
#1978= IFCPROPERTYSINGLEVALUE('rescs',$,IFCNUMERICMEASURE(12.),$);
|
||||
#1982= IFCPROPERTYSINGLEVALUE('gs_shadow',$,IFCNUMERICMEASURE(1.),$);
|
||||
#1986= IFCPROPERTYSINGLEVALUE('AC_show2DHotspotsIn3D',$,IFCNUMERICMEASURE(0.),$);
|
||||
#1990= IFCPROPERTYSINGLEVALUE('gs_cont_pen',$,IFCNUMERICMEASURE(2.),$);
|
||||
#1994= IFCPROPERTYSINGLEVALUE('gs_fill_type',$,IFCNUMERICMEASURE(65.),$);
|
||||
#1998= IFCPROPERTYSINGLEVALUE('gs_fill_pen',$,IFCNUMERICMEASURE(91.),$);
|
||||
#2002= IFCPROPERTYSINGLEVALUE('gs_back_pen',$,IFCNUMERICMEASURE(91.),$);
|
||||
#2006= IFCPROPERTYSINGLEVALUE('sa',$,IFCNUMERICMEASURE(1.),$);
|
||||
#2010= IFCPROPERTYSINGLEVALUE('al',$,IFCNUMERICMEASURE(6.),$);
|
||||
#2014= IFCPROPERTYSINGLEVALUE('fmat',$,IFCNUMERICMEASURE(15.),$);
|
||||
#2018= IFCPROPERTYSINGLEVALUE('pmat',$,IFCNUMERICMEASURE(15.),$);
|
||||
#2022= IFCPROPERTYSINGLEVALUE('bmat',$,IFCNUMERICMEASURE(15.),$);
|
||||
#2026= IFCPROPERTYSINGLEVALUE('gs_list_cost',$,IFCNUMERICMEASURE(0.),$);
|
||||
#2031= IFCPROPERTYSINGLEVALUE('gs_list_manufacturer',$,IFCLABEL(''),$);
|
||||
#2035= IFCPROPERTYSINGLEVALUE('gs_list_note',$,IFCLABEL(''),$);
|
||||
#2039= IFCPROPERTYSINGLEVALUE('gs_list_location',$,IFCLABEL(''),$);
|
||||
#2043= IFCPROPERTYSINGLEVALUE('gs_list_accessories',$,IFCLABEL(''),$);
|
||||
#2047= IFCPROPERTYSINGLEVALUE('FM_Type',$,IFCLABEL('Others'),$);
|
||||
#2051= IFCPROPERTYSINGLEVALUE('FM_InventoryNumber',$,IFCLABEL(''),$);
|
||||
#2055= IFCPROPERTYSINGLEVALUE('FM_SerialNumber',$,IFCLABEL(''),$);
|
||||
#2059= IFCPROPERTYSINGLEVALUE('FM_ProductionYear',$,IFCLABEL(''),$);
|
||||
#2063= IFCPROPERTYSINGLEVALUE('FM_ObjectWeight',$,IFCNUMERICMEASURE(0.),$);
|
||||
#2067= IFCPROPERTYSINGLEVALUE('FM_ObjectWeightUnit',$,IFCLABEL('kg'),$);
|
||||
#2071= IFCPROPERTYSINGLEVALUE('gs_list_custom1',$,IFCLABEL(''),$);
|
||||
#2075= IFCPROPERTYSINGLEVALUE('gs_list_custom2',$,IFCLABEL(''),$);
|
||||
#2079= IFCPROPERTYSINGLEVALUE('gs_list_custom3',$,IFCLABEL(''),$);
|
||||
#2083= IFCPROPERTYSINGLEVALUE('gs_list_custom4',$,IFCLABEL(''),$);
|
||||
#2087= IFCPROPERTYSINGLEVALUE('gs_list_custom5',$,IFCLABEL(''),$);
|
||||
#2091= IFCPROPERTYSINGLEVALUE('gs_detlevel_3d_m',$,IFCNUMERICMEASURE(2.),$);
|
||||
#2095= IFCPROPERTYSINGLEVALUE('ptyp_m',$,IFCNUMERICMEASURE(1.),$);
|
||||
#2099= IFCPROPERTYSINGLEVALUE('rom_m',$,IFCNUMERICMEASURE(1.),$);
|
||||
#2103= IFCPROPERTYSINGLEVALUE('rmm_m',$,IFCNUMERICMEASURE(1.),$);
|
||||
#2107= IFCPROPERTYSINGLEVALUE('hrt_m',$,IFCNUMERICMEASURE(1.),$);
|
||||
#2111= IFCPROPERTYSINGLEVALUE('pt_m',$,IFCNUMERICMEASURE(1.),$);
|
||||
#2115= IFCPROPERTYSINGLEVALUE('bt_m',$,IFCNUMERICMEASURE(1.),$);
|
||||
#2119= IFCCOMPLEXPROPERTY('LIBPARAM',$,'ArchiCAD',(#1805,#1809,#1813,#1817,#1821,#1825,#1829,#1833,#1837,#1841,#1845,#1849,#1853,#1857,#1861,#1865,#1869,#1873,#1877,#1881,#1885,#1889,#1893,#1897,#1902,#1906,#1910,#1914,#1918,#1922,#1926,#1930,#1934,#1938,#1942,#1946,#1950,#1954,#1958,#1962,#1966,#1970,#1974,#1978,#1982,#1986,#1990,#1994,#1998,#2002,#2006,#2010,#2014,#2018,#2022,#2026,#2031,#2035,#2039,#2043,#2047,#2051,#2055,#2059,#2063,#2067,#2071,#2075,#2079,#2083,#2087,#2091,#2095,#2099,#2103,#2107,#2111,#2115));
|
||||
#2125= IFCRELDEFINESBYPROPERTIES('3kqdyy5N1ETPObqZJVJPbH',#13,'ArchiCAD','ExtendedProperties',(#1771),#1800);
|
||||
#2127= IFCPROPERTYSINGLEVALUE('Layername',$,IFCLABEL('Furniture & Equipment'),$);
|
||||
#2131= IFCPROPERTYSINGLEVALUE('Red',$,IFCINTEGER(2),$);
|
||||
#2135= IFCPROPERTYSINGLEVALUE('Green',$,IFCINTEGER(157),$);
|
||||
#2139= IFCPROPERTYSINGLEVALUE('Blue',$,IFCINTEGER(33),$);
|
||||
#2143= IFCCOMPLEXPROPERTY('Color',$,'Color',(#2131,#2135,#2139));
|
||||
#2148= IFCPROPERTYSET('2Sp8gWvUj9yBMn9klggsIS',#13,'PSet_Draughting',$,(#2127,#2143));
|
||||
#2153= IFCRELDEFINESBYPROPERTIES('1PVkHJilvBSxvjXlwjL27x',#13,$,$,(#1771),#2148);
|
||||
#2155= IFCMATERIAL('50 %');
|
||||
#2159= IFCRELASSOCIATESMATERIAL('1GhOPX9UHE9fErZyyZyEzx',#13,$,$,(#1771),#2155);
|
||||
#2161= IFCSLAB('1_g0UhnsD11uLx01z1JWyj',#13,'Slab-004',$,$,#2224,#2213,$,.FLOOR.);
|
||||
#2180= IFCCARTESIANPOINT((0.,0.));
|
||||
#2184= IFCCARTESIANPOINT((1939.3575,0.));
|
||||
#2188= IFCCARTESIANPOINT((1939.3575,945.02591));
|
||||
#2192= IFCCARTESIANPOINT((0.,945.02591));
|
||||
#2196= IFCPOLYLINE((#2180,#2184,#2188,#2192,#2180));
|
||||
#2200= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#2196);
|
||||
#2201= IFCAXIS2PLACEMENT3D(#40,#36,#28);
|
||||
#2204= IFCEXTRUDEDAREASOLID(#2200,#2201,#36,300.);
|
||||
#2207= IFCSHAPEREPRESENTATION(#51,'Body','SweptSolid',(#2204));
|
||||
#2213= IFCPRODUCTDEFINITIONSHAPE($,$,(#2207));
|
||||
#2217= IFCCARTESIANPOINT((0.,0.,-300.));
|
||||
#2221= IFCAXIS2PLACEMENT3D(#2217,#36,#28);
|
||||
#2224= IFCLOCALPLACEMENT(#77,#2221);
|
||||
#2227= IFCAXIS2PLACEMENT3D(#40,#36,#28);
|
||||
#2230= IFCMATERIAL('Wood');
|
||||
#2233= IFCMATERIALLAYER(#2230,20.,.U.);
|
||||
#2235= IFCMATERIAL('Lightweight Concrete');
|
||||
#2238= IFCMATERIALLAYER(#2235,50.,.U.);
|
||||
#2240= IFCMATERIAL('Batt Insulation');
|
||||
#2243= IFCMATERIALLAYER(#2240,30.,.U.);
|
||||
#2245= IFCMATERIAL('Structural Concrete');
|
||||
#2248= IFCMATERIALLAYER(#2245,200.,.U.);
|
||||
#2250= IFCMATERIALLAYERSET((#2233,#2238,#2243,#2248),'concrete floor, insul+parquet');
|
||||
#2252= IFCMATERIALLAYERSETUSAGE(#2250,.AXIS3.,.NEGATIVE.,300.);
|
||||
#2253= IFCRELASSOCIATESMATERIAL('3eTSGAwqf69xWG27aIAN0$',#13,$,$,(#2161),#2252);
|
||||
#2255= IFCPROPERTYSINGLEVALUE('ArchiCAD_UID',$,IFCINTEGER(237),$);
|
||||
#2259= IFCCOMPLEXPROPERTY('SLAB',$,'ArchiCAD',(#2255));
|
||||
#2264= IFCPROPERTYSET('1EehdkAGTAR8C1OYr_OYbq',#13,'Graphisoft AC90 SLAB','Graphisoft AC90',(#2259));
|
||||
#2269= IFCRELDEFINESBYPROPERTIES('2fg$8mogD3RuMTkPATPAUY',#13,'ArchiCAD','ExtendedProperties',(#2161),#2264);
|
||||
#2271= IFCPROPERTYSINGLEVALUE('Layername',$,IFCLABEL('Floors'),$);
|
||||
#2275= IFCPROPERTYSINGLEVALUE('Red',$,IFCINTEGER(204),$);
|
||||
#2279= IFCPROPERTYSINGLEVALUE('Green',$,IFCINTEGER(101),$);
|
||||
#2283= IFCPROPERTYSINGLEVALUE('Blue',$,IFCINTEGER(0),$);
|
||||
#2288= IFCCOMPLEXPROPERTY('Color',$,'Color',(#2275,#2279,#2283));
|
||||
#2293= IFCPROPERTYSET('1iB4HrvuP5BQ0yWb3LjOaM',#13,'PSet_Draughting',$,(#2271,#2288));
|
||||
#2298= IFCRELDEFINESBYPROPERTIES('2dvo0puPT2wPMPbhcPM1zN',#13,$,$,(#2161),#2293);
|
||||
#2300= IFCRELCONTAINEDINSPATIALSTRUCTURE('2fZhnPphjAfunQkj2tDz6W',#13,'BuildingStoreyContainer','BuildingStoreyContainer for Building Elements',(#1771,#2161),#80);
|
||||
#2302= IFCRELAGGREGATES('1$$MJTMxn3_8AeVGnO8szo',#13,'BuildingContainer','BuildingContainer for BuildigStories',#64,(#80));
|
||||
#2304= IFCRELAGGREGATES('1c7zmztwX08w$enkKE9lJ6',#13,'ProjectContainer','ProjectContainer for Sites',#54,(#64));
|
||||
ENDSEC;
|
||||
|
||||
END-ISO-10303-21;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,421 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('IFC2X3.exp'),'2;1');
|
||||
FILE_NAME('C:\\TeklaStructuresModels\\Acis_Sat\\plate_steel_example-tek_1fix.ifc','2006-05-12T10:07:38',('Steel2 macro version:12.0 Build:179423,2.5.2006'),('Structural Designer'),'EXPRESS Data Manager version:20040806','Tekla Structures 12.0','');
|
||||
FILE_SCHEMA(('IFC2X3'));
|
||||
ENDSEC;
|
||||
|
||||
DATA;
|
||||
#1= IFCPERSON('TEKLAAD/lli','Undefined',$,$,$,$,$,$);
|
||||
#3= IFCORGANIZATION($,'Tekla Corporation',$,$,$);
|
||||
#7= IFCPERSONANDORGANIZATION(#1,#3,$);
|
||||
#8= IFCAPPLICATION(#3,'12.0','Tekla Structures','Multi material modeling');
|
||||
#9= IFCOWNERHISTORY(#7,#8,$,.ADDED.,$,$,$,1147417657);
|
||||
#10= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
|
||||
#11= IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#12= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#13= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
|
||||
#14= IFCSIUNIT(*,.SOLIDANGLEUNIT.,$,.STERADIAN.);
|
||||
#15= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
|
||||
#16= IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
|
||||
#17= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.);
|
||||
#18= IFCSIUNIT(*,.LUMINOUSINTENSITYUNIT.,$,.LUMEN.);
|
||||
#19= IFCUNITASSIGNMENT((#10,#11,#12,#13,#14,#15,#16,#17,#18));
|
||||
#21= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#25= IFCDIRECTION((1.,0.,0.));
|
||||
#29= IFCDIRECTION((0.,1.,0.));
|
||||
#33= IFCDIRECTION((0.,0.,1.));
|
||||
#37= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#40= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Design',3,1.0000000E-5,#37,$);
|
||||
#43= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Sketch',3,1.0000000E-5,#37,$);
|
||||
#46= IFCPROJECT('2gPUQOiNz2FR1H6lWQ8j0k',#9,'PROJ: NAME','Description','Object type','LongName','Phase',(#40,#43),#19);
|
||||
#53= IFCMATERIAL('A36');
|
||||
#56= IFCMATERIAL('A992');
|
||||
#59= IFCMATERIAL('A500-GR.B');
|
||||
#62= IFCPERSON('TEKLAAD/chke','Undefined',$,$,$,$,$,$);
|
||||
#64= IFCPERSONANDORGANIZATION(#62,#3,$);
|
||||
#65= IFCOWNERHISTORY(#64,#8,$,.ADDED.,$,$,$,1147417657);
|
||||
#66= IFCSITE('1_WapmNXfFdOqQ3garaDJn',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$,$,$,$,$);
|
||||
#76= IFCRELAGGREGATES('36yaCMhuT2DxfpF3ieGira',#65,$,$,#46,(#66));
|
||||
#78= IFCBUILDING('2iHnVT4$n9JQE18R_2cJEI',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$,$,$);
|
||||
#88= IFCRELAGGREGATES('0A543zcq1Fdv_lsrHHX8Kv',#65,$,$,#66,(#78));
|
||||
#90= IFCBUILDINGSTOREY('0UguZM0$L8L9OUA114_ZEd',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$);
|
||||
#100= IFCRELAGGREGATES('09$Ux3Y999qBeEKV8wCsfA',#65,$,$,#78,(#90));
|
||||
#102= IFCPOLYLINE((#106,#111,#115,#119,#123,#127,#131));
|
||||
#106= IFCCARTESIANPOINT((0.,318.8494));
|
||||
#111= IFCCARTESIANPOINT((95.250002,318.8494));
|
||||
#115= IFCCARTESIANPOINT((120.65,293.4494));
|
||||
#119= IFCCARTESIANPOINT((120.65,25.4));
|
||||
#123= IFCCARTESIANPOINT((95.250002,0.));
|
||||
#127= IFCCARTESIANPOINT((0.,0.));
|
||||
#131= IFCCARTESIANPOINT((0.,318.8494));
|
||||
#135= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL19.1',#102);
|
||||
#136= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#139= IFCEXTRUDEDAREASOLID(#135,#136,#33,19.1);
|
||||
#142= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#146= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#139));
|
||||
#152= IFCPRODUCTDEFINITIONSHAPE($,$,(#146));
|
||||
#156= IFCCARTESIANPOINT((5240.3064,7493.,-337.7327));
|
||||
#160= IFCDIRECTION((1.,0.,0.));
|
||||
#164= IFCDIRECTION((0.,1.,0.));
|
||||
#168= IFCAXIS2PLACEMENT3D(#156,#160,#164);
|
||||
#171= IFCLOCALPLACEMENT($,#168);
|
||||
#174= IFCPLATE('13c6Dt0003iJ4nCpGmDZ4q',#65,'PLATE','PL19.1',$,#171,#152,$);
|
||||
#193= IFCRELCONTAINEDINSPATIALSTRUCTURE('0119IaMyb8cxCzo7l7Qnbw',#65,$,$,(#174,#268,#350,#434,#521,#633,#720,#808,#895,#982,#1078,#1193,#1385,#1473),#90);
|
||||
#195= IFCSTRUCTURALPROFILEPROPERTIES('PL19.1',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#196= IFCPOLYLINE((#200,#204,#208,#212,#216,#220,#224));
|
||||
#200= IFCCARTESIANPOINT((0.,293.4494));
|
||||
#204= IFCCARTESIANPOINT((25.4,318.8494));
|
||||
#208= IFCCARTESIANPOINT((120.65,318.8494));
|
||||
#212= IFCCARTESIANPOINT((120.65,0.));
|
||||
#216= IFCCARTESIANPOINT((25.4,0.));
|
||||
#220= IFCCARTESIANPOINT((0.,25.4));
|
||||
#224= IFCCARTESIANPOINT((0.,293.4494));
|
||||
#228= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL19.1',#196);
|
||||
#229= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#232= IFCEXTRUDEDAREASOLID(#228,#229,#33,19.1);
|
||||
#235= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#240= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#232));
|
||||
#246= IFCPRODUCTDEFINITIONSHAPE($,$,(#240));
|
||||
#250= IFCCARTESIANPOINT((5240.3064,7626.35,-337.7327));
|
||||
#254= IFCDIRECTION((1.,0.,0.));
|
||||
#258= IFCDIRECTION((0.,1.,0.));
|
||||
#262= IFCAXIS2PLACEMENT3D(#250,#254,#258);
|
||||
#265= IFCLOCALPLACEMENT($,#262);
|
||||
#268= IFCPLATE('13c6Dt0003gZ4nCpGmDZ4q',#65,'PLATE','PL19.1',$,#265,#246,$);
|
||||
#287= IFCPOLYLINE((#291,#295,#299,#303,#307));
|
||||
#291= IFCCARTESIANPOINT((0.,0.));
|
||||
#295= IFCCARTESIANPOINT((0.,318.45248));
|
||||
#299= IFCCARTESIANPOINT((99.695007,318.45248));
|
||||
#303= IFCCARTESIANPOINT((99.695007,0.));
|
||||
#307= IFCCARTESIANPOINT((0.,0.));
|
||||
#311= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#287);
|
||||
#312= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#315= IFCEXTRUDEDAREASOLID(#311,#312,#33,9.5);
|
||||
#318= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#322= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#315));
|
||||
#328= IFCPRODUCTDEFINITIONSHAPE($,$,(#322));
|
||||
#332= IFCCARTESIANPOINT((4411.98,7492.5555,-342.9125));
|
||||
#336= IFCDIRECTION((0.,0.,-1.));
|
||||
#340= IFCDIRECTION((0.,1.,0.));
|
||||
#344= IFCAXIS2PLACEMENT3D(#332,#336,#340);
|
||||
#347= IFCLOCALPLACEMENT($,#344);
|
||||
#350= IFCPLATE('13c6Dt0002ip4nCpGmDZ4o',#65,'PLATE','PL9.5',$,#347,#328,$);
|
||||
#370= IFCSTRUCTURALPROFILEPROPERTIES('PL9.5',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#371= IFCPOLYLINE((#375,#379,#383,#387,#391));
|
||||
#375= IFCCARTESIANPOINT((0.,0.));
|
||||
#379= IFCCARTESIANPOINT((0.,318.45248));
|
||||
#383= IFCCARTESIANPOINT((99.695007,318.45248));
|
||||
#387= IFCCARTESIANPOINT((99.695007,0.));
|
||||
#391= IFCCARTESIANPOINT((0.,0.));
|
||||
#395= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#371);
|
||||
#396= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#399= IFCEXTRUDEDAREASOLID(#395,#396,#33,9.5);
|
||||
#402= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#406= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#399));
|
||||
#412= IFCPRODUCTDEFINITIONSHAPE($,$,(#406));
|
||||
#416= IFCCARTESIANPOINT((4411.98,7492.5555,-28.5875));
|
||||
#420= IFCDIRECTION((0.,0.,-1.));
|
||||
#424= IFCDIRECTION((0.,1.,0.));
|
||||
#428= IFCAXIS2PLACEMENT3D(#416,#420,#424);
|
||||
#431= IFCLOCALPLACEMENT($,#428);
|
||||
#434= IFCPLATE('13c6Dt0002hp4nCpGmDZ4o',#65,'PLATE','PL9.5',$,#431,#412,$);
|
||||
#453= IFCCARTESIANPOINT((0.,0.));
|
||||
#457= IFCDIRECTION((1.,0.));
|
||||
#461= IFCAXIS2PLACEMENT2D(#453,#457);
|
||||
#464= IFCRECTANGLEPROFILEDEF(.AREA.,$,#461,9.5,234.9);
|
||||
#465= IFCDIRECTION((0.,0.,-1.));
|
||||
#469= IFCDIRECTION((-1.,0.,0.));
|
||||
#473= IFCAXIS2PLACEMENT3D(#21,#469,#33);
|
||||
#476= IFCEXTRUDEDAREASOLID(#464,#473,#465,304.8);
|
||||
#479= IFCCARTESIANPOINT((0.,-117.45,-4.75));
|
||||
#483= IFCBOUNDINGBOX(#479,304.8,234.9,9.5);
|
||||
#486= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#476));
|
||||
#492= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#483));
|
||||
#499= IFCPRODUCTDEFINITIONSHAPE($,$,(#486,#492));
|
||||
#503= IFCCARTESIANPOINT((4581.8225,7496.2,-342.9));
|
||||
#507= IFCDIRECTION((-1.,0.,0.));
|
||||
#511= IFCDIRECTION((0.,0.,1.));
|
||||
#515= IFCAXIS2PLACEMENT3D(#503,#507,#511);
|
||||
#518= IFCLOCALPLACEMENT($,#515);
|
||||
#521= IFCCOLUMN('13c6Dt0002c34nCpGmDZ4o',#65,'PLATE','PL9.5X234.9',$,#518,#499,$);
|
||||
#540= IFCSTRUCTURALPROFILEPROPERTIES('PL9.5X234.9',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#541= IFCCARTESIANPOINT((0.,0.));
|
||||
#545= IFCDIRECTION((1.,0.));
|
||||
#549= IFCAXIS2PLACEMENT2D(#541,#545);
|
||||
#552= IFCISHAPEPROFILEDEF(.AREA.,$,#549,177.41901,402.84399,9.5249996,10.922,17.653);
|
||||
#553= IFCDIRECTION((0.,0.,-1.));
|
||||
#557= IFCDIRECTION((-1.,0.,0.));
|
||||
#561= IFCAXIS2PLACEMENT3D(#21,#557,#33);
|
||||
#564= IFCEXTRUDEDAREASOLID(#552,#561,#553,2278.7992);
|
||||
#567= IFCCARTESIANPOINT((0.,-201.422,-88.709503));
|
||||
#571= IFCBOUNDINGBOX(#567,778.79918,402.84399,177.41901);
|
||||
#574= IFCCARTESIANPOINT((778.79918,301.422,0.));
|
||||
#578= IFCDIRECTION((1.,0.,0.));
|
||||
#582= IFCDIRECTION((0.,0.,-1.));
|
||||
#586= IFCAXIS2PLACEMENT3D(#574,#578,#582);
|
||||
#589= IFCPLANE(#586);
|
||||
#592= IFCHALFSPACESOLID(#589,.F.);
|
||||
#595= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#564,#592);
|
||||
#598= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#595));
|
||||
#604= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#571));
|
||||
#610= IFCPRODUCTDEFINITIONSHAPE($,$,(#598,#604));
|
||||
#614= IFCCARTESIANPOINT((4572.,6688.3563,-201.422));
|
||||
#618= IFCDIRECTION((1.,0.,0.));
|
||||
#623= IFCDIRECTION((0.,1.,0.));
|
||||
#627= IFCAXIS2PLACEMENT3D(#614,#618,#623);
|
||||
#630= IFCLOCALPLACEMENT($,#627);
|
||||
#633= IFCBEAM('13c6Dt0001iZ4nCpGmDZ0v',#65,'BEAM','W16X36',$,#630,#610,$);
|
||||
#652= IFCSTRUCTURALPROFILEPROPERTIES('W16X36',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#653= IFCCARTESIANPOINT((0.,0.));
|
||||
#657= IFCDIRECTION((1.,0.));
|
||||
#661= IFCAXIS2PLACEMENT2D(#653,#657);
|
||||
#664= IFCLSHAPEPROFILEDEF(.AREA.,$,#661,101.6,88.900002,9.5249996,9.525,$,$,$,$);
|
||||
#665= IFCDIRECTION((0.,0.,-1.));
|
||||
#669= IFCDIRECTION((-1.,0.,0.));
|
||||
#673= IFCAXIS2PLACEMENT3D(#21,#669,#33);
|
||||
#676= IFCEXTRUDEDAREASOLID(#664,#673,#665,254.);
|
||||
#679= IFCCARTESIANPOINT((0.,-50.799999,-44.450001));
|
||||
#683= IFCBOUNDINGBOX(#679,254.,101.6,88.900002);
|
||||
#686= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#676));
|
||||
#692= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#683));
|
||||
#698= IFCPRODUCTDEFINITIONSHAPE($,$,(#686,#692));
|
||||
#702= IFCCARTESIANPOINT((4794.758,7562.85,-298.45));
|
||||
#706= IFCDIRECTION((1.,0.,0.));
|
||||
#710= IFCDIRECTION((0.,0.,1.));
|
||||
#714= IFCAXIS2PLACEMENT3D(#702,#706,#710);
|
||||
#717= IFCLOCALPLACEMENT($,#714);
|
||||
#720= IFCCOLUMN('13c6Dt0001GZ4nCpGmDZ0u',#65,'ANGLE','L4X3-1/2X3/8',$,#717,#698,$);
|
||||
#739= IFCSTRUCTURALPROFILEPROPERTIES('L4X3-1/2X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#740= IFCCARTESIANPOINT((0.,0.));
|
||||
#744= IFCDIRECTION((1.,0.));
|
||||
#748= IFCAXIS2PLACEMENT2D(#740,#744);
|
||||
#752= IFCLSHAPEPROFILEDEF(.AREA.,$,#748,101.6,88.900002,9.5249996,9.525,$,$,$,$);
|
||||
#753= IFCDIRECTION((0.,0.,-1.));
|
||||
#757= IFCDIRECTION((-1.,0.,0.));
|
||||
#761= IFCAXIS2PLACEMENT3D(#21,#757,#33);
|
||||
#764= IFCEXTRUDEDAREASOLID(#752,#761,#753,254.);
|
||||
#767= IFCCARTESIANPOINT((0.,-50.799999,-44.450001));
|
||||
#771= IFCBOUNDINGBOX(#767,254.,101.6,88.900002);
|
||||
#774= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#764));
|
||||
#780= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#771));
|
||||
#786= IFCPRODUCTDEFINITIONSHAPE($,$,(#774,#780));
|
||||
#790= IFCCARTESIANPOINT((4794.758,7677.15,-44.45));
|
||||
#794= IFCDIRECTION((1.,0.,0.));
|
||||
#798= IFCDIRECTION((0.,0.,-1.));
|
||||
#802= IFCAXIS2PLACEMENT3D(#790,#794,#798);
|
||||
#805= IFCLOCALPLACEMENT($,#802);
|
||||
#808= IFCBEAM('13c6Dt0001FZ4nCpGmDZ0u',#65,'ANGLE','L4X3-1/2X3/8',$,#805,#786,$);
|
||||
#827= IFCCARTESIANPOINT((0.,0.));
|
||||
#831= IFCDIRECTION((1.,0.));
|
||||
#835= IFCAXIS2PLACEMENT2D(#827,#831);
|
||||
#838= IFCLSHAPEPROFILEDEF(.AREA.,$,#835,101.6,101.60025,9.5249996,9.525,$,$,$,$);
|
||||
#839= IFCDIRECTION((0.,0.,-1.));
|
||||
#843= IFCDIRECTION((-1.,0.,0.));
|
||||
#847= IFCAXIS2PLACEMENT3D(#21,#843,#33);
|
||||
#850= IFCEXTRUDEDAREASOLID(#838,#847,#839,292.1);
|
||||
#853= IFCCARTESIANPOINT((0.,-50.799999,-50.800125));
|
||||
#857= IFCBOUNDINGBOX(#853,292.1,101.6,101.60025);
|
||||
#860= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#850));
|
||||
#866= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#857));
|
||||
#872= IFCPRODUCTDEFINITIONSHAPE($,$,(#860,#866));
|
||||
#876= IFCCARTESIANPOINT((4915.409,7564.4494,50.799999));
|
||||
#881= IFCDIRECTION((0.,-1.,0.));
|
||||
#885= IFCDIRECTION((1.,0.,0.));
|
||||
#889= IFCAXIS2PLACEMENT3D(#876,#881,#885);
|
||||
#892= IFCLOCALPLACEMENT($,#889);
|
||||
#895= IFCBEAM('13c6Dt0000F34nCpGmDZ0s',#65,'BEAM','L4X4X3/8',$,#892,#872,$);
|
||||
#914= IFCSTRUCTURALPROFILEPROPERTIES('L4X4X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#915= IFCCARTESIANPOINT((0.,0.));
|
||||
#919= IFCDIRECTION((1.,0.));
|
||||
#923= IFCAXIS2PLACEMENT2D(#915,#919);
|
||||
#926= IFCLSHAPEPROFILEDEF(.AREA.,$,#923,101.6,101.60025,9.5249996,9.525,$,$,$,$);
|
||||
#927= IFCDIRECTION((0.,0.,-1.));
|
||||
#931= IFCDIRECTION((-1.,0.,0.));
|
||||
#935= IFCAXIS2PLACEMENT3D(#21,#931,#33);
|
||||
#938= IFCEXTRUDEDAREASOLID(#926,#935,#927,330.2);
|
||||
#941= IFCCARTESIANPOINT((0.,-50.799999,-50.800125));
|
||||
#945= IFCBOUNDINGBOX(#941,330.2,101.6,101.60025);
|
||||
#948= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#938));
|
||||
#954= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#945));
|
||||
#960= IFCPRODUCTDEFINITIONSHAPE($,$,(#948,#954));
|
||||
#964= IFCCARTESIANPOINT((4801.108,7564.4494,495.301));
|
||||
#968= IFCDIRECTION((0.,-1.,0.));
|
||||
#972= IFCDIRECTION((0.,0.,-1.));
|
||||
#976= IFCAXIS2PLACEMENT3D(#964,#968,#972);
|
||||
#979= IFCLOCALPLACEMENT($,#976);
|
||||
#982= IFCBEAM('13c6Dt00006J4nCpGmDZ0s',#65,'BEAM','L4X4X3/8',$,#979,#960,$);
|
||||
#1001= IFCPOLYLINE((#1006,#1010,#1014,#1018,#1022,#1026));
|
||||
#1006= IFCCARTESIANPOINT((0.,0.));
|
||||
#1010= IFCCARTESIANPOINT((-0.001,486.84845));
|
||||
#1014= IFCCARTESIANPOINT((369.07135,486.84845));
|
||||
#1018= IFCCARTESIANPOINT((512.75543,343.16437));
|
||||
#1022= IFCCARTESIANPOINT((512.75543,0.));
|
||||
#1026= IFCCARTESIANPOINT((0.,0.));
|
||||
#1030= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#1001);
|
||||
#1031= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#1034= IFCEXTRUDEDAREASOLID(#1030,#1031,#33,9.5);
|
||||
#1037= IFCCARTESIANPOINT((-0.0010532137,0.,-4.75));
|
||||
#1041= IFCBOUNDINGBOX(#1037,486.8495,512.75614,9.5);
|
||||
#1044= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1034));
|
||||
#1050= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1041));
|
||||
#1056= IFCPRODUCTDEFINITIONSHAPE($,$,(#1044,#1050));
|
||||
#1060= IFCCARTESIANPOINT((4763.008,7615.262,12.701));
|
||||
#1064= IFCDIRECTION((0.,1.,0.));
|
||||
#1068= IFCDIRECTION((0.,0.,1.));
|
||||
#1072= IFCAXIS2PLACEMENT3D(#1060,#1064,#1068);
|
||||
#1075= IFCLOCALPLACEMENT($,#1072);
|
||||
#1078= IFCPLATE('13c6Dt00005J4nCpGmDZ0s',#65,'PLATE','PL9.5',$,#1075,#1056,$);
|
||||
#1097= IFCCARTESIANPOINT((0.,0.));
|
||||
#1101= IFCDIRECTION((1.,0.));
|
||||
#1105= IFCAXIS2PLACEMENT2D(#1097,#1101);
|
||||
#1108= IFCRECTANGLEHOLLOWPROFILEDEF(.AREA.,$,#1105,152.39999,152.39999,9.5249996,9.5250004,19.05);
|
||||
#1109= IFCDIRECTION((0.,0.,-1.));
|
||||
#1113= IFCDIRECTION((-1.,0.,0.));
|
||||
#1117= IFCAXIS2PLACEMENT3D(#1130,#1113,#33);
|
||||
#1120= IFCEXTRUDEDAREASOLID(#1108,#1117,#1109,2177.4228);
|
||||
#1123= IFCCARTESIANPOINT((-1.2141754E-10,-76.199997,-76.199997));
|
||||
#1127= IFCBOUNDINGBOX(#1123,677.4228,152.39999,152.39999);
|
||||
#1130= IFCCARTESIANPOINT((-1500.,0.,0.));
|
||||
#1135= IFCCARTESIANPOINT((-1.0913936E-10,-9.0949470E-12,4.7379999));
|
||||
#1139= IFCDIRECTION((-1.,-1.6052583E-13,0.));
|
||||
#1143= IFCDIRECTION((-1.6052583E-13,1.,0.));
|
||||
#1147= IFCAXIS2PLACEMENT3D(#1135,#1139,#1143);
|
||||
#1150= IFCPLANE(#1147);
|
||||
#1153= IFCHALFSPACESOLID(#1150,.F.);
|
||||
#1156= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#1120,#1153);
|
||||
#1159= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#1156));
|
||||
#1165= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1127));
|
||||
#1171= IFCPRODUCTDEFINITIONSHAPE($,$,(#1159,#1165));
|
||||
#1175= IFCCARTESIANPOINT((5007.3895,7620.,282.98953));
|
||||
#1179= IFCDIRECTION((0.,-1.,0.));
|
||||
#1183= IFCDIRECTION((0.70710678,0.,0.70710678));
|
||||
#1187= IFCAXIS2PLACEMENT3D(#1175,#1179,#1183);
|
||||
#1190= IFCLOCALPLACEMENT($,#1187);
|
||||
#1193= IFCBEAM('13c6Dt00003p4nCpGmDZ0r',#65,'BRACE','TS6X6X3/8',$,#1190,#1171,$);
|
||||
#1212= IFCCARTESIANPOINT((5153.6546,7620.012,307.34937));
|
||||
#1216= IFCDIRECTION((0.,-1.,0.));
|
||||
#1220= IFCDIRECTION((-0.70710678,0.,0.70710678));
|
||||
#1224= IFCAXIS2PLACEMENT3D(#1212,#1216,#1220);
|
||||
#1227= IFCLOCALPLACEMENT($,#1224);
|
||||
#1230= IFCCARTESIANPOINT((0.,0.));
|
||||
#1234= IFCDIRECTION((1.,0.));
|
||||
#1238= IFCAXIS2PLACEMENT2D(#1230,#1234);
|
||||
#1241= IFCRECTANGLEPROFILEDEF(.AREA.,$,#1238,9.5,266.7);
|
||||
#1242= IFCDIRECTION((0.,0.,-1.));
|
||||
#1246= IFCDIRECTION((-1.,0.,0.));
|
||||
#1250= IFCAXIS2PLACEMENT3D(#21,#1246,#33);
|
||||
#1253= IFCEXTRUDEDAREASOLID(#1241,#1250,#1242,172.39997);
|
||||
#1256= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1253));
|
||||
#1263= IFCPRODUCTDEFINITIONSHAPE($,$,(#1256));
|
||||
#1267= IFCOPENINGELEMENT('13c6Dt0000Ip4nCpGmDZ0s',#65,$,$,$,#1227,#1263,$);
|
||||
#1288= IFCRELVOIDSELEMENT('3gz_1FcMHExhEMJzH8zl$M',#65,$,$,#1193,#1267);
|
||||
#1289= IFCSTRUCTURALPROFILEPROPERTIES('TS6X6X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#1290= IFCCARTESIANPOINT((0.,0.));
|
||||
#1294= IFCDIRECTION((1.,0.));
|
||||
#1298= IFCAXIS2PLACEMENT2D(#1290,#1294);
|
||||
#1301= IFCISHAPEPROFILEDEF(.AREA.,$,#1298,254.88901,356.616,12.7,18.288,21.3995);
|
||||
#1302= IFCDIRECTION((0.,0.,-1.));
|
||||
#1306= IFCDIRECTION((-1.,0.,0.));
|
||||
#1310= IFCAXIS2PLACEMENT3D(#1323,#1306,#33);
|
||||
#1313= IFCEXTRUDEDAREASOLID(#1301,#1310,#1302,2768.2595);
|
||||
#1316= IFCCARTESIANPOINT((0.,-178.308,-127.4445));
|
||||
#1320= IFCBOUNDINGBOX(#1316,1268.2595,356.616,254.88901);
|
||||
#1323= IFCCARTESIANPOINT((-1500.,0.,0.));
|
||||
#1327= IFCCARTESIANPOINT((0.,278.308,0.));
|
||||
#1331= IFCDIRECTION((-1.,0.,0.));
|
||||
#1335= IFCDIRECTION((0.,0.,1.));
|
||||
#1339= IFCAXIS2PLACEMENT3D(#1327,#1331,#1335);
|
||||
#1342= IFCPLANE(#1339);
|
||||
#1345= IFCHALFSPACESOLID(#1342,.F.);
|
||||
#1348= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#1313,#1345);
|
||||
#1351= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#1348));
|
||||
#1357= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1320));
|
||||
#1363= IFCPRODUCTDEFINITIONSHAPE($,$,(#1351,#1357));
|
||||
#1367= IFCCARTESIANPOINT((4763.008,7620.,-178.308));
|
||||
#1371= IFCDIRECTION((0.,-1.,0.));
|
||||
#1375= IFCDIRECTION((1.,0.,-7.2832534E-15));
|
||||
#1379= IFCAXIS2PLACEMENT3D(#1367,#1371,#1375);
|
||||
#1382= IFCLOCALPLACEMENT($,#1379);
|
||||
#1385= IFCBEAM('13c6Dt00002p4nCpGmDZ0r',#65,'BEAM','W14X68',$,#1382,#1363,$);
|
||||
#1405= IFCSTRUCTURALPROFILEPROPERTIES('W14X68',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#1406= IFCCARTESIANPOINT((0.,0.));
|
||||
#1410= IFCDIRECTION((1.,0.));
|
||||
#1414= IFCAXIS2PLACEMENT2D(#1406,#1410);
|
||||
#1417= IFCISHAPEPROFILEDEF(.AREA.,$,#1414,254.88901,356.616,12.7,18.288,21.3995);
|
||||
#1418= IFCDIRECTION((0.,0.,-1.));
|
||||
#1422= IFCDIRECTION((-1.,0.,0.));
|
||||
#1426= IFCAXIS2PLACEMENT3D(#21,#1422,#33);
|
||||
#1429= IFCEXTRUDEDAREASOLID(#1417,#1426,#1418,1524.);
|
||||
#1432= IFCCARTESIANPOINT((0.,-178.308,-127.4445));
|
||||
#1436= IFCBOUNDINGBOX(#1432,1524.,356.616,254.88901);
|
||||
#1439= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1429));
|
||||
#1445= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1436));
|
||||
#1451= IFCPRODUCTDEFINITIONSHAPE($,$,(#1439,#1445));
|
||||
#1455= IFCCARTESIANPOINT((4572.,7620.,-609.6));
|
||||
#1459= IFCDIRECTION((0.,-1.,0.));
|
||||
#1463= IFCDIRECTION((7.1613756E-15,0.,1.));
|
||||
#1467= IFCAXIS2PLACEMENT3D(#1455,#1459,#1463);
|
||||
#1470= IFCLOCALPLACEMENT($,#1467);
|
||||
#1473= IFCCOLUMN('13c6Dt00001p4nCpGmDZ0q',#65,'COLUMN','W14X68',$,#1470,#1451,$);
|
||||
#1492= IFCCARTESIANPOINT((4661.154,7490.05,308.83104));
|
||||
#1496= IFCDIRECTION((0.,1.,0.));
|
||||
#1500= IFCDIRECTION((-1.,0.,7.0134555E-15));
|
||||
#1504= IFCAXIS2PLACEMENT3D(#1492,#1496,#1500);
|
||||
#1507= IFCLOCALPLACEMENT($,#1504);
|
||||
#1510= IFCPOLYLINE((#1514,#1519,#1523,#1527,#1531,#1535,#1539,#1543,#1547,#1551,#1555,#1559,#1563,#1567,#1571,#1575,#1579,#1583,#1587,#1591,#1595,#1599,#1603,#1607,#1611,#1615,#1619,#1623,#1627,#1631,#1635,#1639,#1643,#1648,#1652,#1656,#1660));
|
||||
#1514= IFCCARTESIANPOINT((0.38429439,16.098194));
|
||||
#1519= IFCCARTESIANPOINT((0.,20.));
|
||||
#1523= IFCCARTESIANPOINT((0.,336.616));
|
||||
#1527= IFCCARTESIANPOINT((0.38429439,340.5178));
|
||||
#1531= IFCCARTESIANPOINT((1.5224093,344.26967));
|
||||
#1535= IFCCARTESIANPOINT((3.3706078,347.7274));
|
||||
#1539= IFCCARTESIANPOINT((5.8578644,350.75813));
|
||||
#1543= IFCCARTESIANPOINT((8.8885953,353.24539));
|
||||
#1547= IFCCARTESIANPOINT((12.346331,355.09359));
|
||||
#1551= IFCCARTESIANPOINT((16.098194,356.2317));
|
||||
#1555= IFCCARTESIANPOINT((20.,356.616));
|
||||
#1559= IFCCARTESIANPOINT((158.308,356.616));
|
||||
#1563= IFCCARTESIANPOINT((162.20981,356.2317));
|
||||
#1567= IFCCARTESIANPOINT((165.96167,355.09359));
|
||||
#1571= IFCCARTESIANPOINT((169.4194,353.24539));
|
||||
#1575= IFCCARTESIANPOINT((172.45013,350.75813));
|
||||
#1579= IFCCARTESIANPOINT((174.93739,347.7274));
|
||||
#1583= IFCCARTESIANPOINT((176.78559,344.26967));
|
||||
#1587= IFCCARTESIANPOINT((177.9237,340.5178));
|
||||
#1591= IFCCARTESIANPOINT((178.308,336.616));
|
||||
#1595= IFCCARTESIANPOINT((178.308,20.));
|
||||
#1599= IFCCARTESIANPOINT((177.9237,16.098194));
|
||||
#1603= IFCCARTESIANPOINT((176.78559,12.346331));
|
||||
#1607= IFCCARTESIANPOINT((174.93739,8.8885953));
|
||||
#1611= IFCCARTESIANPOINT((172.45013,5.8578644));
|
||||
#1615= IFCCARTESIANPOINT((169.4194,3.3706078));
|
||||
#1619= IFCCARTESIANPOINT((165.96167,1.5224093));
|
||||
#1623= IFCCARTESIANPOINT((162.20981,0.38429439));
|
||||
#1627= IFCCARTESIANPOINT((158.308,0.));
|
||||
#1631= IFCCARTESIANPOINT((20.,0.));
|
||||
#1635= IFCCARTESIANPOINT((16.098194,0.38429439));
|
||||
#1639= IFCCARTESIANPOINT((12.346331,1.5224093));
|
||||
#1643= IFCCARTESIANPOINT((8.8885953,3.3706078));
|
||||
#1648= IFCCARTESIANPOINT((5.8578644,5.8578644));
|
||||
#1652= IFCCARTESIANPOINT((3.3706078,8.8885953));
|
||||
#1656= IFCCARTESIANPOINT((1.5224093,12.346331));
|
||||
#1660= IFCCARTESIANPOINT((0.38429439,16.098194));
|
||||
#1664= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL259.9',#1510);
|
||||
#1665= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#1668= IFCEXTRUDEDAREASOLID(#1664,#1665,#33,259.9);
|
||||
#1671= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1668));
|
||||
#1677= IFCPRODUCTDEFINITIONSHAPE($,$,(#1671));
|
||||
#1681= IFCOPENINGELEMENT('13c6Dt0002m34nCpGmDZ4o',#65,$,$,$,#1507,#1677,$);
|
||||
#1702= IFCRELVOIDSELEMENT('2WI1gpRhj1dwB5Pft5vGvO',#65,$,$,#1473,#1681);
|
||||
#1703= IFCRELASSOCIATESMATERIAL('1GOaWfy957kRCtp2R28$a9',#9,$,$,(#1193),#59);
|
||||
#1705= IFCRELASSOCIATESMATERIAL('3Vqwp2DmT3TeKWHZyFzO7N',#9,$,$,(#1473,#1385,#633),#56);
|
||||
#1707= IFCRELASSOCIATESMATERIAL('0SMrmHf_5BRAYDVih2VNTN',#9,$,$,(#1078,#982,#895,#808,#720,#521,#434,#350,#268,#174),#53);
|
||||
#1709= IFCRELASSOCIATESPROFILEPROPERTIES('2mzP0x3pj2GwUm7Wy7Xfs0',#9,$,$,(#633),#652,$,$);
|
||||
#1711= IFCRELASSOCIATESPROFILEPROPERTIES('1iUJLoCsfD6ug6keUDRGVW',#9,$,$,(#808),#739,$,$);
|
||||
#1713= IFCRELASSOCIATESPROFILEPROPERTIES('1LidMEK_vEQuG8Js1MD1ND',#9,$,$,(#895,#982),#914,$,$);
|
||||
#1715= IFCRELASSOCIATESPROFILEPROPERTIES('22dp9maVb0sAxZG8F2xTIR',#9,$,$,(#1193),#1289,$,$);
|
||||
#1717= IFCRELASSOCIATESPROFILEPROPERTIES('36LPEeuXn6VgX3frK_urjy',#9,$,$,(#1385),#1405,$,$);
|
||||
ENDSEC;
|
||||
|
||||
END-ISO-10303-21;
|
||||
@@ -0,0 +1,47 @@
|
||||
const fs = require( 'fs' )
|
||||
|
||||
const TMP_RESULTS_PATH = '/tmp/import_result.json'
|
||||
|
||||
const { parseAndCreateCommit } = require( './index' )
|
||||
|
||||
async function main() {
|
||||
let cmdArgs = process.argv.slice( 2 )
|
||||
|
||||
let [ filePath, userId, streamId, branchName, commitMessage ] = cmdArgs
|
||||
|
||||
console.log( 'ARGV: ', filePath, userId, streamId, branchName, commitMessage )
|
||||
|
||||
const data = fs.readFileSync( filePath )
|
||||
|
||||
let ifcInput = {
|
||||
data,
|
||||
streamId: streamId,
|
||||
userId: userId,
|
||||
message: commitMessage || 'Imported file'
|
||||
}
|
||||
if ( branchName ) ifcInput.branchName = branchName
|
||||
|
||||
let output = {
|
||||
success: false,
|
||||
error: 'Unknown error'
|
||||
}
|
||||
|
||||
try {
|
||||
let commitId = await parseAndCreateCommit( ifcInput )
|
||||
output = {
|
||||
success: true,
|
||||
commitId
|
||||
}
|
||||
} catch ( err ) {
|
||||
output = {
|
||||
success: false,
|
||||
error: err.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync( TMP_RESULTS_PATH, JSON.stringify( output ) )
|
||||
|
||||
process.exit( 0 )
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,54 @@
|
||||
const fetch = require( 'node-fetch' )
|
||||
const Parser = require( './parser' )
|
||||
const ServerAPI = require( './api.js' )
|
||||
|
||||
async function parseAndCreateCommit( { data, streamId, branchName = 'uploads', userId, message = 'Manual IFC file upload' } ) {
|
||||
const serverApi = new ServerAPI( { streamId } )
|
||||
const myParser = new Parser( { serverApi } )
|
||||
|
||||
const { id, tCount } = await myParser.parse( data )
|
||||
|
||||
let commit = {
|
||||
streamId: streamId,
|
||||
branchName: branchName,
|
||||
objectId: id,
|
||||
message: message,
|
||||
sourceApplication: 'IFC',
|
||||
totalChildrenCount: tCount
|
||||
}
|
||||
|
||||
let branch = await serverApi.getBranchByNameAndStreamId( { streamId: streamId, name: branchName } )
|
||||
|
||||
if( !branch ) {
|
||||
await serverApi.createBranch( {
|
||||
name: branchName,
|
||||
streamId: streamId,
|
||||
description: branchName === 'uploads' ? 'File upload branch' : null,
|
||||
authorId: userId
|
||||
} )
|
||||
}
|
||||
|
||||
let userToken = process.env.USER_TOKEN
|
||||
|
||||
let server_base_url = process.env.SPECKLE_SERVER_URL || 'http://localhost:3000'
|
||||
const response = await fetch( server_base_url + '/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${userToken}`
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
query: 'mutation createCommit( $myCommitInput: CommitCreateInput!) { commitCreate( commit: $myCommitInput ) }',
|
||||
variables:{
|
||||
myCommitInput: commit
|
||||
}
|
||||
} )
|
||||
} )
|
||||
|
||||
let json = await response.json()
|
||||
console.log( json )
|
||||
|
||||
return json.data.commitCreate
|
||||
}
|
||||
|
||||
module.exports = { parseAndCreateCommit }
|
||||
@@ -0,0 +1,251 @@
|
||||
const WebIFC = require( 'web-ifc/web-ifc-api-node' )
|
||||
const ServerAPI = require( './api.js' )
|
||||
|
||||
module.exports = class IFCParser {
|
||||
|
||||
constructor( { serverApi } ) {
|
||||
this.api = new WebIFC.IfcAPI()
|
||||
this.serverApi = serverApi || new ServerAPI()
|
||||
}
|
||||
|
||||
async parse( data ) {
|
||||
if ( this.api.wasmModule === undefined ) await this.api.Init()
|
||||
|
||||
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.project = this.api.GetLine( this.modelId, this.projectId, true )
|
||||
this.project.__closure = {}
|
||||
|
||||
this.cache = {}
|
||||
this.closureCache = {}
|
||||
|
||||
// Steps: create and store in speckle all the geometries (meshes) from this project and store them
|
||||
// as reference objects in this.productGeo
|
||||
this.productGeo = {}
|
||||
await this.createGeometries()
|
||||
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
|
||||
// replace all its geometries with actual references to speckle meshes from the productGeo map
|
||||
|
||||
await this.traverse( this.project, true, 0 )
|
||||
|
||||
let id = await this.serverApi.saveObject( this.project )
|
||||
return { id, tCount: Object.keys( this.project.__closure ).length }
|
||||
}
|
||||
|
||||
async createGeometries() {
|
||||
this.rawGeo = this.api.LoadAllGeometry( this.modelId )
|
||||
|
||||
for( let i = 0; i < this.rawGeo.size(); i++ ) {
|
||||
const mesh = this.rawGeo.get( i )
|
||||
const prodId = mesh.expressID
|
||||
this.productGeo[prodId ] = []
|
||||
|
||||
for( let j = 0; j < mesh.geometries.size(); j++ ) {
|
||||
let placedGeom = mesh.geometries.get( j )
|
||||
let geom = this.api.GetGeometry( this.modelId, placedGeom.geometryExpressID )
|
||||
|
||||
let matrix = placedGeom.flatTransformation
|
||||
let raw = {
|
||||
color: geom.color, // NOTE: material: x, y, z = rgb, w = opacity
|
||||
vertices: this.api.GetVertexArray( geom.GetVertexData(), geom.GetVertexDataSize() ),
|
||||
indices: this.api.GetIndexArray( geom.GetIndexData(), geom.GetIndexDataSize() )
|
||||
}
|
||||
|
||||
const { vertices } = this.extractVertexData( raw.vertices )
|
||||
|
||||
for( let k = 0; k < vertices.length; k += 3 ) {
|
||||
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 + 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]
|
||||
}
|
||||
|
||||
// Since all faces are triangles, we must add a `0` before each group of 3.
|
||||
let spcklFaces = [ ]
|
||||
for ( let i = 0; i < raw.indices.length; i++ ) {
|
||||
if( i % 3 === 0 )
|
||||
spcklFaces.push( 0 )
|
||||
spcklFaces.push( raw.indices[i] )
|
||||
}
|
||||
|
||||
// Create a propper Speckle Mesh
|
||||
let spcklMesh = {
|
||||
speckle_type: 'Objects.Geometry.Mesh',
|
||||
units: 'm',
|
||||
volume: 0,
|
||||
area: 0,
|
||||
faces: spcklFaces,
|
||||
vertices: Array.from( vertices ),
|
||||
renderMaterial: placedGeom.color ? this.colorToMaterial( placedGeom.color ) : null
|
||||
}
|
||||
|
||||
let id = await this.serverApi.saveObject( spcklMesh )
|
||||
let ref = { speckle_type: 'reference', referencedId: id }
|
||||
this.productGeo[prodId].push( ref )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async traverse( element, recursive = true, depth = 0 ) {
|
||||
|
||||
// Fast exit if null/undefined
|
||||
if ( !element ) return
|
||||
|
||||
|
||||
// If array, traverse all items in it.
|
||||
if( Array.isArray( element ) ) {
|
||||
return element.map( async el => await this.traverse( el,recursive, depth + 1 ) )
|
||||
}
|
||||
|
||||
// If it has no expressID, its either a simple type or a { type, value } object.
|
||||
if( !element.expressID ) {
|
||||
return element.value !== null && element.value !== undefined ? element.value : element
|
||||
}
|
||||
|
||||
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.
|
||||
// console.log( `Traversing element ${element.expressID}; Recurse: ${recursive}; Stack ${depth}` )
|
||||
|
||||
// Traverse all key/value pairs first.
|
||||
for( let key of Object.keys( element ) ) {
|
||||
element[key] = await this.traverse( element[key], recursive, depth + 1 )
|
||||
}
|
||||
|
||||
// Assign speckle_type and empty closure table.
|
||||
element.speckle_type = element.constructor.name
|
||||
element.__closure = {}
|
||||
|
||||
// Find spatial children and assign to element
|
||||
const spatialChildrenIds = this.getAllRelatedItemsOfType( 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
|
||||
const childrenIds = this.getAllRelatedItemsOfType( element.expressID, WebIFC.IFCRELCONTAINEDINSPATIALSTRUCTURE, 'RelatingStructure', 'RelatedElements' )
|
||||
if( childrenIds.length > 0 ) element.rawChildren = childrenIds.map( ( childId ) => this.api.GetLine( this.modelId, childId, true ) )
|
||||
|
||||
// Lookup geometry in generated geometries object
|
||||
if( this.productGeo[element.expressID] ) {
|
||||
element['@displayValue'] = this.productGeo[element.expressID]
|
||||
this.productGeo[element.expressID].forEach( ref => {
|
||||
this.project.__closure[ref.referencedId.toString()] = depth
|
||||
element.__closure[ref.referencedId.toString()] = 1
|
||||
} )
|
||||
}
|
||||
|
||||
// Recurse all children
|
||||
if ( recursive ) {
|
||||
|
||||
if( element.rawSpatialChildren ) {
|
||||
element.spatialChildren = []
|
||||
for( let child of element.rawSpatialChildren ) {
|
||||
let res = await this.traverse( child, recursive, depth + 1 )
|
||||
if( res.referencedId ) {
|
||||
element.spatialChildren.push( res )
|
||||
this.project.__closure[res.referencedId.toString()] = depth
|
||||
element.__closure[res.referencedId.toString()] = 1
|
||||
|
||||
// adds to parent (this element) the child's closure tree.
|
||||
if( this.closureCache[child.expressID.toString()] ) {
|
||||
for( let key of Object.keys( this.closureCache[child.expressID.toString()] ) ) {
|
||||
element.__closure[key] = this.closureCache[child.expressID.toString()][key] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete element.rawSpatialChildren
|
||||
}
|
||||
|
||||
if ( element.rawChildren ) {
|
||||
element.children = []
|
||||
for( let child of element.rawChildren ) {
|
||||
let res = await this.traverse( child, recursive, depth + 1 )
|
||||
if( res.referencedId ) {
|
||||
element.children.push( res )
|
||||
this.project.__closure[res.referencedId.toString()] = depth
|
||||
element.__closure[res.referencedId.toString()] = 1
|
||||
|
||||
// adds to parent (this element) the child's closure tree.
|
||||
if( this.closureCache[child.expressID.toString()] ) {
|
||||
for( let key of Object.keys( this.closureCache[child.expressID.toString()] ) ) {
|
||||
element.__closure[key] = this.closureCache[child.expressID.toString()][key] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete element.rawChildren
|
||||
}
|
||||
|
||||
if( element.children || element.spatialChildren ) {
|
||||
console.log( `${element.constructor.name} ${element.GlobalId}: children count: ${ element.children ? element.children.length : '0'}; spatial children count: ${element.spatialChildren ? element.spatialChildren.length : '0'} ` )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if( this.productGeo[element.expressID] || element.spatialChildren || element.children ) {
|
||||
let id = await this.serverApi.saveObject( element )
|
||||
let ref = { speckle_type: 'reference', referencedId: id }
|
||||
this.cache[element.expressID.toString()] = ref
|
||||
this.closureCache[element.expressID.toString()] = element.__closure
|
||||
return ref
|
||||
} else {
|
||||
this.cache[element.expressID.toString()] = element
|
||||
this.closureCache[element.expressID.toString()] = element.__closure
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// (c) https://github.com/agviegas/web-ifc-three
|
||||
extractVertexData( vertexData ) {
|
||||
const vertices = []
|
||||
const normals = []
|
||||
let isNormalData = false
|
||||
for ( let i = 0; i < vertexData.length; i++ ) {
|
||||
isNormalData ? normals.push( vertexData[i] ) : vertices.push( vertexData[i] )
|
||||
if ( ( i + 1 ) % 3 === 0 ) isNormalData = !isNormalData
|
||||
}
|
||||
return { vertices, normals }
|
||||
}
|
||||
|
||||
// (c) https://github.com/agviegas/web-ifc-three/blob/907e08b5673d5e1c18261a4fceade7189d6b2db7/src/IFC/PropertyManager.ts#L110
|
||||
getAllRelatedItemsOfType( elementID, type, relation, relatedProperty ) {
|
||||
const lines = this.api.GetLineIDsWithType( this.modelId, type )
|
||||
const IDs = []
|
||||
|
||||
for ( let i = 0; i < lines.size(); i++ ) {
|
||||
const relID = lines.get( i )
|
||||
const rel = this.api.GetLine( this.modelId, relID )
|
||||
const relatedItems = rel[relation]
|
||||
let foundElement = false
|
||||
|
||||
if ( Array.isArray( relatedItems ) ) {
|
||||
const values = relatedItems.map( ( item ) => item.value )
|
||||
foundElement = values.includes( elementID )
|
||||
} else foundElement = ( relatedItems.value === elementID )
|
||||
|
||||
if ( foundElement ) {
|
||||
const element = rel[relatedProperty]
|
||||
if ( !Array.isArray( element ) ) IDs.push( element.value )
|
||||
else element.forEach( ( ele ) => IDs.push( ele.value ) )
|
||||
}
|
||||
}
|
||||
|
||||
return IDs
|
||||
}
|
||||
|
||||
colorToMaterial( color ) {
|
||||
let intColor = ( color.w << 24 ) + ( ( color.x * 255 ) << 16 ) + ( ( color.y * 255 ) << 8 ) + ( ( color.z * 255 ) )
|
||||
|
||||
return {
|
||||
diffuse: intColor,
|
||||
opacity: color.w,
|
||||
metalness: 0,
|
||||
roughness: 1,
|
||||
speckle_type: 'Objects.Other.RenderMaterial'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = require( 'knex' )( {
|
||||
client: 'pg',
|
||||
connection: process.env.PG_CONNECTION_STRING || 'postgres://speckle:speckle@localhost/speckle',
|
||||
pool: { min: 1, max: 1 }
|
||||
// migrations are in managed in the server package
|
||||
} )
|
||||
+5701
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@speckle/fileimport-service",
|
||||
"version": "2.0.0",
|
||||
"description": "Parse and import files of various types into a stream",
|
||||
"author": "Dimitrie Stefanescu <didimitrie@gmail.com>",
|
||||
"homepage": "https://github.com/specklesystems/speckle-server#readme",
|
||||
"license": "SEE LICENSE IN readme.md",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/specklesystems/speckle-server.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nodemon ./src/daemon.js"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/specklesystems/speckle-server/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.996.0",
|
||||
"bcrypt": "^5.0.1",
|
||||
"crypto-random-string": "^3.3.1",
|
||||
"eslint": "^7.29.0",
|
||||
"knex": "^0.95.11",
|
||||
"node-fetch": "^2.6.5",
|
||||
"pg": "^8.7.1",
|
||||
"web-ifc": "0.0.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.13"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
'use strict'
|
||||
|
||||
const crypto = require( 'crypto' )
|
||||
const knex = require( '../knex' )
|
||||
|
||||
const { getFileStream } = require( './filesApi' )
|
||||
const fs = require( 'fs' )
|
||||
const { spawn } = require( 'child_process' )
|
||||
|
||||
const ServerAPI = require( '../ifc/api' )
|
||||
|
||||
const TMP_FILE_PATH = '/tmp/file_to_import'
|
||||
const TMP_RESULTS_PATH = '/tmp/import_result.json'
|
||||
|
||||
async function startTask() {
|
||||
let { rows } = await knex.raw( `
|
||||
UPDATE file_uploads
|
||||
SET
|
||||
"convertedStatus" = 1,
|
||||
"convertedLastUpdate" = NOW()
|
||||
FROM (
|
||||
SELECT "id" FROM file_uploads
|
||||
WHERE "convertedStatus" = 0 AND "uploadComplete" = 't'
|
||||
ORDER BY "convertedLastUpdate" ASC
|
||||
LIMIT 1
|
||||
) as task
|
||||
WHERE file_uploads."id" = task."id"
|
||||
RETURNING file_uploads."id"
|
||||
` )
|
||||
return rows[0]
|
||||
}
|
||||
|
||||
async function doTask( task ) {
|
||||
let tempUserToken = null
|
||||
let serverApi = null
|
||||
|
||||
try {
|
||||
console.log( 'Doing task ', task )
|
||||
let { rows } = await knex.raw( `
|
||||
SELECT
|
||||
id as "fileId", "streamId", "branchName", "userId", "fileName", "fileType"
|
||||
FROM file_uploads
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`, [ task.id ] )
|
||||
let info = rows[0]
|
||||
if ( !info ) {
|
||||
throw new Error( 'Internal error: DB inconsistent' )
|
||||
}
|
||||
|
||||
let upstreamFileStream = await getFileStream( { fileId: info.fileId } )
|
||||
let diskFileStream = fs.createWriteStream( TMP_FILE_PATH )
|
||||
|
||||
upstreamFileStream.pipe( diskFileStream )
|
||||
|
||||
await new Promise( fulfill => diskFileStream.on( 'finish' , fulfill ) )
|
||||
|
||||
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 } )
|
||||
tempUserToken = token
|
||||
|
||||
await runProcessWithTimeout(
|
||||
'node',
|
||||
[
|
||||
'./ifc/import_file.js',
|
||||
TMP_FILE_PATH,
|
||||
info.userId,
|
||||
info.streamId,
|
||||
info.branchName,
|
||||
`File upload: ${info.fileName}`
|
||||
],
|
||||
{
|
||||
USER_TOKEN: tempUserToken
|
||||
},
|
||||
10 * 60 * 1000
|
||||
)
|
||||
|
||||
let output = JSON.parse( fs.readFileSync( TMP_RESULTS_PATH ) )
|
||||
|
||||
if ( !output.success )
|
||||
throw new Error( output.error )
|
||||
|
||||
let commitId = output.commitId
|
||||
|
||||
await knex.raw( `
|
||||
UPDATE file_uploads
|
||||
SET
|
||||
"convertedStatus" = 2,
|
||||
"convertedLastUpdate" = NOW(),
|
||||
"convertedMessage" = 'File converted successfully',
|
||||
"convertedCommitId" = ?
|
||||
WHERE "id" = ?
|
||||
`, [ commitId, task.id ] )
|
||||
} catch ( err ) {
|
||||
console.log( 'Error: ', err )
|
||||
await knex.raw( `
|
||||
UPDATE file_uploads
|
||||
SET
|
||||
"convertedStatus" = 3,
|
||||
"convertedLastUpdate" = NOW(),
|
||||
"convertedMessage" = ?
|
||||
WHERE "id" = ?
|
||||
`, [ err.toString(), task.id ] )
|
||||
}
|
||||
|
||||
if ( fs.existsSync( TMP_FILE_PATH ) ) fs.unlinkSync( TMP_FILE_PATH )
|
||||
if ( fs.existsSync( TMP_RESULTS_PATH ) ) fs.unlinkSync( TMP_RESULTS_PATH )
|
||||
|
||||
if ( tempUserToken ) {
|
||||
await serverApi.revokeTokenById( tempUserToken )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 } } )
|
||||
|
||||
childProc.stdout.on( 'data', ( data ) => {
|
||||
console.log( 'Parser: ', data.toString() )
|
||||
} )
|
||||
|
||||
childProc.stderr.on( 'data', ( data ) => {
|
||||
console.error( 'Parser: ', data.toString() )
|
||||
} )
|
||||
|
||||
let timedOut = false
|
||||
|
||||
let timeout = setTimeout( () => {
|
||||
console.log( 'Process timeout. Killing process...' )
|
||||
|
||||
timedOut = true
|
||||
childProc.kill( 9 )
|
||||
reject( `Timeout: Process took longer than ${timeoutMs} ms to execute` )
|
||||
}, timeoutMs )
|
||||
|
||||
childProc.on( 'close', ( code ) => {
|
||||
console.log( `Process exited with code ${code}` )
|
||||
|
||||
if ( timedOut ) return // ignore `close` calls after killing (the promise was already rejected)
|
||||
|
||||
clearTimeout( timeout )
|
||||
|
||||
if ( code === 0 ) {
|
||||
resolve()
|
||||
} else {
|
||||
reject( `Parser exited with code ${code}` )
|
||||
}
|
||||
} )
|
||||
|
||||
} )
|
||||
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
try {
|
||||
let task = await startTask()
|
||||
if ( !task ) {
|
||||
setTimeout( tick, 1000 )
|
||||
return
|
||||
}
|
||||
|
||||
await doTask( task )
|
||||
|
||||
// Check for another task very soon
|
||||
setTimeout( tick, 10 )
|
||||
} catch ( err ) {
|
||||
console.log( 'Error executing task: ', err )
|
||||
setTimeout( tick, 5000 )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function main() {
|
||||
console.log( 'Starting FileUploads Service...' )
|
||||
tick()
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,40 @@
|
||||
/* istanbul ignore file */
|
||||
'use strict'
|
||||
|
||||
const S3 = require( 'aws-sdk/clients/s3' )
|
||||
|
||||
function getS3Config()
|
||||
{
|
||||
// TODO: use ENV
|
||||
return {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY || 'minioadmin',
|
||||
secretAccessKey: process.env.S3_SECRET_KEY || 'minioadmin',
|
||||
endpoint: process.env.S3_ENDPOINT || 'http://127.0.0.1:9000',
|
||||
s3ForcePathStyle: true,
|
||||
signatureVersion: 'v4'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
|
||||
async getFileStream( { fileId } ) {
|
||||
const s3 = new S3( getS3Config() )
|
||||
let Bucket = process.env.S3_BUCKET
|
||||
let Key = `files/${fileId}`
|
||||
|
||||
let fileStream = s3.getObject( { Key, Bucket } ).createReadStream()
|
||||
return fileStream
|
||||
},
|
||||
|
||||
async readFile( { fileId } ) {
|
||||
const s3 = new S3( getS3Config() )
|
||||
let Bucket = process.env.S3_BUCKET
|
||||
let Key = `files/${fileId}`
|
||||
|
||||
let s3Data = await s3.getObject( { Key, Bucket } ).promise()
|
||||
|
||||
return s3Data.Body
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<v-card class="my-4 elevation-1" :loading="$apollo.loading">
|
||||
<div v-if="!$apollo.loading && file">
|
||||
<v-toolbar dense flat color="transparent">
|
||||
<v-app-bar-nav-icon
|
||||
v-tooltip="`Download the original file`"
|
||||
@click="downloadOriginalFile()"
|
||||
>
|
||||
<v-icon>mdi-download</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title>
|
||||
{{ file.fileName }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<template v-if="file.convertedStatus === 0">
|
||||
<v-btn text disabled>
|
||||
<span class="mr-2">Queued</span>
|
||||
<v-progress-circular indeterminate :size="20" :width="2"></v-progress-circular>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-if="file.convertedStatus === 1">
|
||||
<v-btn text>
|
||||
<span class="mr-2">Converting</span>
|
||||
<v-progress-circular indeterminate :size="20" :width="2"></v-progress-circular>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-if="file.convertedStatus === 2">
|
||||
<v-btn
|
||||
text
|
||||
color="primary"
|
||||
:to="`/streams/${$route.params.streamId}/commits/${file.convertedCommitId}`"
|
||||
>
|
||||
<span class="mr-2">View Commit</span>
|
||||
<v-icon class="">mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-if="file.convertedStatus === 3">
|
||||
<v-btn v-tooltip="file.convertedMessage" text>
|
||||
<span class="mr-2 error--text">Error</span>
|
||||
<v-icon color="error">mdi-bug</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-toolbar>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-skeleton-loader
|
||||
class="mx-auto"
|
||||
max-width="300"
|
||||
type="list-item-one-line"
|
||||
></v-skeleton-loader>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
fileId: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
percentCompleted: -1,
|
||||
error: null,
|
||||
file: null
|
||||
}),
|
||||
apollo: {
|
||||
file: {
|
||||
query: gql`
|
||||
query File($id: String!, $streamId: String!) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
fileUpload(id: $id) {
|
||||
id
|
||||
convertedCommitId
|
||||
userId
|
||||
convertedStatus
|
||||
convertedMessage
|
||||
fileName
|
||||
fileType
|
||||
uploadComplete
|
||||
uploadDate
|
||||
convertedLastUpdate
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.fileId,
|
||||
streamId: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return !this.fileId
|
||||
},
|
||||
update: (data) => data.stream.fileUpload
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
file(val) {
|
||||
if (val.convertedStatus >= 2) this.$apollo.queries.file.stopPolling()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$apollo.queries.file.startPolling(1000)
|
||||
},
|
||||
methods: {
|
||||
async downloadOriginalFile() {
|
||||
let res = await fetch(`/api/file/${this.fileId}`, {
|
||||
headers: {
|
||||
Authorization: localStorage.getItem('AuthToken')
|
||||
}
|
||||
})
|
||||
let blob = await res.blob()
|
||||
let file = window.URL.createObjectURL(blob)
|
||||
|
||||
let a = document.createElement('a')
|
||||
document.body.appendChild(a)
|
||||
a.style = 'display: none'
|
||||
a.href = file
|
||||
a.download = this.file.fileName
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<v-card v-if="file" class="my-4 elevation-1" :loading="percentCompleted != -1">
|
||||
<template slot="progress">
|
||||
<v-progress-linear color="primary" height="4" :value="percentCompleted"></v-progress-linear>
|
||||
</template>
|
||||
<v-toolbar flat color="transparent">
|
||||
<v-toolbar-title>
|
||||
{{ file.name }}
|
||||
<span class="caption">{{ file.size }}kb</span>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu offset-y>
|
||||
<template #activator="{ attrs, 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>
|
||||
<span class="caption">{{ selectedBranch }}</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="item in branches.filter((b) => b.name != 'globals')"
|
||||
:key="item.name"
|
||||
link
|
||||
@click="selectedBranch = item.name"
|
||||
>
|
||||
<v-list-item-title class="caption">{{ item.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn color="primary" @click="upload()">Upload</v-btn>
|
||||
</v-toolbar>
|
||||
<v-alert v-if="error" type="error" dismissible>An error occurred.</v-alert>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['file', 'branches'],
|
||||
data: () => ({
|
||||
percentCompleted: -1,
|
||||
error: null,
|
||||
selectedBranch: 'main'
|
||||
}),
|
||||
methods: {
|
||||
upload() {
|
||||
let data = new FormData()
|
||||
this.error = null
|
||||
data.append('file', this.file)
|
||||
|
||||
let request = new XMLHttpRequest()
|
||||
request.open(
|
||||
'POST',
|
||||
`/api/file/ifc/${this.$route.params.streamId}/${
|
||||
this.selectedBranch ? this.selectedBranch : 'main'
|
||||
}`
|
||||
)
|
||||
request.setRequestHeader('Authorization', `Bearer ${localStorage.getItem('AuthToken')}`)
|
||||
|
||||
request.upload.addEventListener(
|
||||
'progress',
|
||||
function (e) {
|
||||
this.percentCompleted = (e.loaded / e.total) * 100
|
||||
if (this.percentCompleted >= 100) {
|
||||
this.$emit('done', this.file.name)
|
||||
}
|
||||
}.bind(this)
|
||||
)
|
||||
|
||||
// request finished event
|
||||
request.addEventListener(
|
||||
'load',
|
||||
function () {
|
||||
if (request.status !== 200) {
|
||||
this.error = request.response
|
||||
}
|
||||
|
||||
this.$emit('done', this.file.name)
|
||||
}.bind(this)
|
||||
)
|
||||
|
||||
request.addEventListener(
|
||||
'error',
|
||||
function () {
|
||||
if (request.status !== 200) {
|
||||
this.error = request.response
|
||||
}
|
||||
}.bind(this)
|
||||
)
|
||||
try {
|
||||
request.send(data)
|
||||
} catch (e) {
|
||||
this.error = 'There was an error: ' + e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -3,10 +3,10 @@
|
||||
<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-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-if="showImage">
|
||||
<v-img contain max-height="200" src="@/assets/emptybox.png"></v-img>
|
||||
</div>
|
||||
<div class=" text-center mb-2 space-grotesk">
|
||||
<div class="text-center mb-2 space-grotesk">
|
||||
<slot name="default"></slot>
|
||||
</div>
|
||||
<v-container style="max-width: 500px">
|
||||
@@ -48,12 +48,12 @@
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="hasManager"
|
||||
link
|
||||
:class="`${hasManager ? 'primary' : ''} mb-4`"
|
||||
dark
|
||||
href="https://speckle.systems/features/connectors"
|
||||
target="_blank"
|
||||
v-if="hasManager"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-swap-horizontal</v-icon>
|
||||
@@ -67,11 +67,11 @@
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="hasManager"
|
||||
link
|
||||
:class="`grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'} mb-4`"
|
||||
href="https://speckle.systems/tutorials"
|
||||
target="_blank"
|
||||
v-if="hasManager"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-school</v-icon>
|
||||
@@ -85,11 +85,11 @@
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="hasManager"
|
||||
link
|
||||
:class="`grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'} mb-4`"
|
||||
href="https://speckle.guide"
|
||||
target="_blank"
|
||||
v-if="hasManager"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-book-open-variant</v-icon>
|
||||
@@ -130,6 +130,12 @@
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
export default {
|
||||
props: {
|
||||
showImage: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
user: {
|
||||
query: gql`
|
||||
@@ -146,7 +152,7 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return{}
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
rootUrl() {
|
||||
@@ -160,14 +166,13 @@ export default {
|
||||
mounted() {
|
||||
this.checkAccountTimer = setInterval(
|
||||
function () {
|
||||
if(!this.hasManager)
|
||||
this.$apollo.queries.user.refetch()
|
||||
if (!this.hasManager) this.$apollo.queries.user.refetch()
|
||||
}.bind(this),
|
||||
3000
|
||||
)
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval( this.checkAccountTimer )
|
||||
clearInterval(this.checkAccountTimer)
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,11 @@
|
||||
<v-dialog v-model="liff" max-width="400" :fullscreen="$vuetify.breakpoint.xsOnly">
|
||||
<v-card>
|
||||
<v-toolbar>
|
||||
<v-toolbar-title>thanks for all the fish <v-icon>mdi-fish</v-icon><v-icon>mdi-arrow-up</v-icon></v-toolbar-title>
|
||||
<v-toolbar-title>
|
||||
thanks for all the fish
|
||||
<v-icon>mdi-fish</v-icon>
|
||||
<v-icon>mdi-arrow-up</v-icon>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="liff = false"><v-icon>mdi-close</v-icon></v-btn>
|
||||
</v-toolbar>
|
||||
@@ -51,6 +55,12 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
gotostreamonclick: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
search: '',
|
||||
liff: false,
|
||||
@@ -85,12 +95,15 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
selectedSearchResult(val) {
|
||||
this.search = ''
|
||||
let myStream = this.streams.items.find((s) => s.id === val.id)
|
||||
this.$emit('select', myStream)
|
||||
|
||||
this.streams.items = []
|
||||
if (val) this.$router.push({ name: 'stream', params: { streamId: val.id } })
|
||||
this.search = ''
|
||||
|
||||
if (val && this.gotostreamonclick) this.$router.push(`/streams/${val.id}`)
|
||||
},
|
||||
search(val) {
|
||||
console.log(val)
|
||||
if (val === '42') this.liff = true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,29 +2,23 @@
|
||||
<div style="display: inline-block">
|
||||
<v-menu v-if="loggedIn" offset-x open-on-hover>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-avatar
|
||||
v-if="userById"
|
||||
class="ma-1"
|
||||
color="grey lighten-3"
|
||||
:size="size"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
|
||||
>
|
||||
<v-img v-if="avatar" :src="avatar" />
|
||||
<v-img v-else :src="`https://robohash.org/` + id + `.png?size=40x40`" />
|
||||
</v-avatar>
|
||||
<div v-if="userById" v-on="on">
|
||||
<user-avatar-icon
|
||||
:size="size"
|
||||
:avatar="avatar"
|
||||
:seed="id"
|
||||
v-bind="attrs"
|
||||
class="ma-1"
|
||||
></user-avatar-icon>
|
||||
</div>
|
||||
<v-avatar v-else class="ma-1" :size="size" v-bind="attrs" v-on="on">
|
||||
<v-img contain src="/logo.svg"></v-img>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-card v-if="userById" style="width: 200px" :to="isSelf ? '/profile' : '/profile/' + id">
|
||||
<v-card-text v-if="!$apollo.loading" class="text-center">
|
||||
<v-avatar class="my-4" color="grey lighten-3" :size="40">
|
||||
<v-img v-if="avatar" :src="avatar" />
|
||||
<v-img v-else :src="`https://robohash.org/` + id + `.png?size=40x40`" />
|
||||
</v-avatar>
|
||||
|
||||
<user-avatar-icon class="my-4" :size="40" :avatar="avatar" :seed="id"></user-avatar-icon>
|
||||
|
||||
<!-- Uncomment when email verification is in place -->
|
||||
<!-- <div v-if="userById.verified" class="mb-1">
|
||||
<v-chip color="primary" small>
|
||||
@@ -37,9 +31,9 @@
|
||||
<b>{{ userById.name }}</b>
|
||||
</div>
|
||||
<div class="caption">
|
||||
{{ userById.company }}
|
||||
<br/>
|
||||
{{ userById.bio ? 'Bio: ' + userById.bio : ''}}
|
||||
{{ userById.company }}
|
||||
<br />
|
||||
{{ userById.bio ? 'Bio: ' + userById.bio : '' }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -51,16 +45,21 @@
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-avatar v-else class="ma-1" color="grey lighten-3" :size="size">
|
||||
<v-img v-if="avatar" :src="avatar" />
|
||||
<v-img v-else :src="`https://robohash.org/` + id + `.png?size=40x40`" />
|
||||
</v-avatar>
|
||||
<user-avatar-icon
|
||||
v-else
|
||||
class="ma-1"
|
||||
:size="size"
|
||||
:avatar="avatar"
|
||||
:seed="id"
|
||||
></user-avatar-icon>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import userByIdQuery from '../graphql/userById.gql'
|
||||
import UserAvatarIcon from '@/components/UserAvatarIcon'
|
||||
|
||||
export default {
|
||||
components: { UserAvatarIcon },
|
||||
props: {
|
||||
avatar: String,
|
||||
name: String,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div v-if="user" style="display: inline-block" class="text-center">
|
||||
<v-avatar class="ma-1" color="grey lighten-3" :size="size">
|
||||
<v-img v-if="user.avatar" :src="user.avatar" />
|
||||
<v-img v-else :src="`https://robohash.org/` + id + `.png?size=40x40`" />
|
||||
</v-avatar>
|
||||
<user-avatar-icon :size="size" :avatar="user.avatar" :seed="user.id"></user-avatar-icon>
|
||||
<p class="text-h6 mt-4">
|
||||
{{ user.name }}
|
||||
<br />
|
||||
@@ -14,8 +11,10 @@
|
||||
<script>
|
||||
import { signOut } from '@/auth-helpers'
|
||||
import userQuery from '../graphql/userById.gql'
|
||||
import UserAvatarIcon from '@/components/UserAvatarIcon'
|
||||
|
||||
export default {
|
||||
components: { UserAvatarIcon },
|
||||
props: {
|
||||
size: {
|
||||
type: Number,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<v-avatar :size="size" color="grey lighten-3">
|
||||
<v-img v-if="avatar" :src="avatar" />
|
||||
<v-img v-else :src="`https://robohash.org/${seed}.png?size=${size}x${size}`" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
seed: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -35,7 +35,11 @@
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`"
|
||||
>
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`">
|
||||
<v-toolbar-title><span v-if="isSelf">Hi </span>{{ user.name }}<span v-if="isSelf">!</span></v-toolbar-title>
|
||||
<v-toolbar-title>
|
||||
<span v-if="isSelf">Hi</span>
|
||||
{{ user.name }}
|
||||
<span v-if="isSelf">!</span>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="isSelf" small rounded color="primary" @click="editUser">
|
||||
<v-icon small class="mr-2">mdi-cog-outline</v-icon>
|
||||
@@ -53,16 +57,15 @@
|
||||
<br />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4" class="d-flex justify-center">
|
||||
<v-avatar
|
||||
class="elevation-0 align-self-center"
|
||||
size="100"
|
||||
@click="avatarDialog = isSelf ? true : false"
|
||||
v-tooltip="`${isSelf ? 'Change your profile picture' : ''}` "
|
||||
:style="`${isSelf ? 'cursor: pointer;' : ''}`"
|
||||
>
|
||||
<v-img v-if="user.avatar" :src="user.avatar" />
|
||||
<v-img v-else :src="`https://robohash.org/` + user.id + `.png?size=64x64`" />
|
||||
</v-avatar>
|
||||
<div @click="avatarDialog = isSelf ? true : false">
|
||||
<user-avatar-icon
|
||||
v-tooltip="`${isSelf ? 'Change your profile picture' : ''}`"
|
||||
:style="`${isSelf ? 'cursor: pointer;' : ''}`"
|
||||
:size="100"
|
||||
:avatar="user.avatar"
|
||||
:seed="user.id"
|
||||
></user-avatar-icon>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions></v-card-actions>
|
||||
@@ -75,9 +78,10 @@
|
||||
import gql from 'graphql-tag'
|
||||
import UserEditDialog from '../components/dialogs/UserEditDialog'
|
||||
import VImageInput from 'vuetify-image-input/a-la-carte'
|
||||
import UserAvatarIcon from '@/components/UserAvatarIcon'
|
||||
|
||||
export default {
|
||||
components: { UserEditDialog, VImageInput },
|
||||
components: { UserAvatarIcon, UserEditDialog, VImageInput },
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
|
||||
@@ -69,13 +69,13 @@ import userSearchQuery from '@/graphql/userSearch.gql'
|
||||
export default {
|
||||
name: 'ServerAdminsCard',
|
||||
components: { SearchBar, ServerAdminsUser },
|
||||
props: ['adminUsers'],
|
||||
data() {
|
||||
return {
|
||||
selectedSearchItem: [],
|
||||
search: '',
|
||||
userSearch: { items: [] },
|
||||
addUserMode: false,
|
||||
adminUsers: []
|
||||
addUserMode: false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<div class="d-flex align-center pa-3 admin-user-view">
|
||||
<div class="d-flex flex-grow-1 align-center">
|
||||
<img height="60pt" width="60pt" class="rounded-circle overflow-hidden elevation-1" contain :src="admin.avatar"/>
|
||||
<img
|
||||
height="60pt"
|
||||
width="60pt"
|
||||
class="rounded-circle overflow-hidden elevation-1"
|
||||
contain
|
||||
:src="admin.avatar"
|
||||
/>
|
||||
<div class="d-flex flex-column flex-grow-1 ml-2" style="min-width: 30%">
|
||||
<span class="subtitle-1">{{ admin.name }}</span>
|
||||
<span class="caption">
|
||||
@@ -52,7 +58,7 @@ export default {
|
||||
admin: {},
|
||||
widgets: {},
|
||||
},
|
||||
data(){
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<v-dialog v-model="show" width="500" @keydown.esc="cancel" :fullscreen="$vuetify.breakpoint.smAndDown">
|
||||
<v-card :loading="loading" v-if="branch && branch.name !== 'main'">
|
||||
<v-dialog
|
||||
v-model="show"
|
||||
width="500"
|
||||
:fullscreen="$vuetify.breakpoint.smAndDown"
|
||||
@keydown.esc="cancel"
|
||||
>
|
||||
<v-card v-if="branch && branch.name !== 'main'" :loading="loading">
|
||||
<v-toolbar color="primary" dark flat>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
@@ -9,7 +14,9 @@
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="show = false"><v-icon>mdi-close</v-icon></v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-alert v-show="error" dismissible type="error">
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="agree">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
@@ -20,6 +27,10 @@
|
||||
required
|
||||
autofocus
|
||||
></v-text-field>
|
||||
<p class="caption">
|
||||
Tip: you can create nested branches by using "/" as a separator in their names. E.g.,
|
||||
"mep/stage-1" or "arch/sketch-design".
|
||||
</p>
|
||||
<v-textarea v-model="branch.description" rows="2" label="Description"></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
@@ -64,6 +75,8 @@ export default {
|
||||
showDelete: false,
|
||||
nameRules: [
|
||||
(v) => !!v || 'Branches need a name too!',
|
||||
(v) =>
|
||||
!(v.startsWith('#') || v.startsWith('/')) || 'Branch names cannot start with "#" or "/"',
|
||||
(v) =>
|
||||
(v && this.allBranchNames.findIndex((e) => e === v) === -1) ||
|
||||
'A branch with this name already exists',
|
||||
@@ -72,7 +85,8 @@ export default {
|
||||
],
|
||||
isEdit: false,
|
||||
pendingDelete: false,
|
||||
allBranchNames: []
|
||||
allBranchNames: [],
|
||||
error: null
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
@@ -118,6 +132,7 @@ export default {
|
||||
methods: {
|
||||
async deleteBranch() {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
this.$matomo && this.$matomo.trackPageView('branch/delete')
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
@@ -134,16 +149,18 @@ export default {
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
this.error = e.message
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
|
||||
this.resolve({
|
||||
result: true,
|
||||
deleted: true
|
||||
})
|
||||
this.dialog = false
|
||||
this.showDelete = false
|
||||
if (!this.error) {
|
||||
this.resolve({
|
||||
result: true,
|
||||
deleted: true
|
||||
})
|
||||
this.dialog = false
|
||||
}
|
||||
},
|
||||
open(branch) {
|
||||
this.dialog = true
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
required
|
||||
autofocus
|
||||
></v-text-field>
|
||||
<p class="caption">
|
||||
Tip: you can create nested branches by using "/" as a separator in their names. E.g.,
|
||||
"mep/stage-1" or "arch/sketch-design".
|
||||
</p>
|
||||
<v-textarea v-model="description" rows="2" label="Description"></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
@@ -42,18 +46,17 @@ export default {
|
||||
showError: false,
|
||||
error: null,
|
||||
streamId: null,
|
||||
branchNames: ['main', 'globals'],
|
||||
reservedBranchNames: ['main', 'globals'],
|
||||
valid: false,
|
||||
loading: false,
|
||||
name: null,
|
||||
name: '',
|
||||
nameRules: [
|
||||
(v) => !!v || 'Branches need a name too!',
|
||||
(v) =>
|
||||
(v && !v.startsWith('globals')) ||
|
||||
'Globals is a reserved branch name. Please choose a different name.',
|
||||
!(v.startsWith('#') || v.startsWith('/')) || 'Branch names cannot start with "#" or "/"',
|
||||
(v) =>
|
||||
(v && this.branchNames.findIndex((e) => e === v) === -1) ||
|
||||
'A branch with this name already exists',
|
||||
(v && this.reservedBranchNames.findIndex((e) => e === v) === -1) ||
|
||||
'This is a reserved branch name',
|
||||
(v) => (v && v.length <= 100) || 'Name must be less than 100 characters',
|
||||
(v) => (v && v.length >= 3) || 'Name must be at least 3 characters'
|
||||
],
|
||||
@@ -61,6 +64,11 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
watch: {
|
||||
name(val) {
|
||||
this.name = val.toLowerCase()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.showDialog = true
|
||||
@@ -80,7 +88,7 @@ export default {
|
||||
variables: {
|
||||
params: {
|
||||
streamId: this.$route.params.streamId,
|
||||
name: this.name,
|
||||
name: this.name.toLowerCase(),
|
||||
description: this.description
|
||||
}
|
||||
}
|
||||
@@ -90,7 +98,9 @@ export default {
|
||||
this.loading = false
|
||||
this.showDialog = false
|
||||
this.$emit('refetch-branches')
|
||||
this.$router.push(`/streams/${this.$route.params.streamId}/branches/${this.name}`)
|
||||
this.$router.push(
|
||||
`/streams/${this.$route.params.streamId}/branches/${this.name.toLowerCase()}`
|
||||
)
|
||||
} catch (err) {
|
||||
this.showError = true
|
||||
if (err.message.includes('branches_streamid_name_unique'))
|
||||
|
||||
@@ -14,13 +14,11 @@
|
||||
v-model="name"
|
||||
:rules="nameRules"
|
||||
validate-on-blur
|
||||
required
|
||||
autofocus
|
||||
label="Stream Name"
|
||||
label="Stream Name (optional)"
|
||||
/>
|
||||
<v-textarea v-model="description" rows="1" row-height="15" label="Description (optional)" />
|
||||
<v-switch
|
||||
inset
|
||||
v-model="isPublic"
|
||||
v-tooltip="
|
||||
isPublic
|
||||
@@ -28,6 +26,7 @@
|
||||
can edit it.`
|
||||
: `Only collaborators can access this stream.`
|
||||
"
|
||||
inset
|
||||
:label="`${isPublic ? 'Public stream' : 'Private stream'}`"
|
||||
/>
|
||||
|
||||
@@ -155,9 +154,12 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.nameRules = [
|
||||
(v) => !!v || 'Stream name is required.',
|
||||
(v) => (v && v.length <= 150) || 'Name must be less than 150 characters',
|
||||
(v) => (v && v.length >= 3) || 'Name must be at least 3 characters'
|
||||
(v) =>
|
||||
!v ||
|
||||
(v.length <= 150 && v.length >= 3) ||
|
||||
'Stream name must be between 3 and 150 characters.'
|
||||
// (v) => (!v && v.length <= 150) || 'Name must be less than 150 characters',
|
||||
// (v) => (!v && v.length >= 3) || 'Name must be at least 3 characters'
|
||||
]
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -57,7 +57,7 @@ const routes = [
|
||||
meta: {
|
||||
title: 'Home | Speckle'
|
||||
},
|
||||
component: () => import('@/views/Frontend_re.vue'),
|
||||
component: () => import('@/views/Frontend.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@@ -80,7 +80,7 @@ const routes = [
|
||||
meta: {
|
||||
title: 'Stream | Speckle'
|
||||
},
|
||||
component: () => import('@/views/stream/Stream_re_re.vue'),
|
||||
component: () => import('@/views/stream/Stream.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@@ -88,13 +88,13 @@ const routes = [
|
||||
meta: {
|
||||
title: 'Stream | Speckle'
|
||||
},
|
||||
component: () => import('@/views/stream/Details_re.vue')
|
||||
component: () => import('@/views/stream/Details.vue')
|
||||
},
|
||||
|
||||
{
|
||||
path: 'branches/',
|
||||
name: 'branches',
|
||||
redirect: 'branches/main',
|
||||
redirect: 'branches/main'
|
||||
},
|
||||
{
|
||||
path: 'branches/:branchName*',
|
||||
@@ -102,7 +102,14 @@ const routes = [
|
||||
meta: {
|
||||
title: 'Branch | Speckle'
|
||||
},
|
||||
component: () => import('@/views/stream/Branch.vue')
|
||||
component: () => import('@/views/stream/Branch.vue'),
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (to.params.branchName.toLowerCase() !== to.params.branchName)
|
||||
return next(
|
||||
`/streams/${to.params.streamId}/branches/${to.params.branchName.toLowerCase()}`
|
||||
)
|
||||
else next()
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'commits/:commitId',
|
||||
@@ -156,6 +163,15 @@ const routes = [
|
||||
props: true,
|
||||
component: () => import('@/views/stream/Webhooks.vue')
|
||||
},
|
||||
{
|
||||
path: 'uploads/',
|
||||
name: 'uploads',
|
||||
meta: {
|
||||
title: 'Stream Uploads | Speckle'
|
||||
},
|
||||
props: true,
|
||||
component: () => import('@/views/stream/Uploads.vue')
|
||||
},
|
||||
{
|
||||
path: 'globals/',
|
||||
name: 'globals',
|
||||
@@ -204,11 +220,12 @@ const routes = [
|
||||
path: 'dashboard',
|
||||
component: () => import('@/views/admin/AdminOverview.vue')
|
||||
},
|
||||
// {
|
||||
// name: 'Admin | Users',
|
||||
// path: 'users',
|
||||
// component: () => import('@/views/admin/AdminUsers.vue')
|
||||
// },
|
||||
{
|
||||
name: 'Admin | Users',
|
||||
path: 'users',
|
||||
component: () => import('@/views/admin/AdminUsers.vue'),
|
||||
props: (route) => ({ ...route.query, ...route.props })
|
||||
},
|
||||
// {
|
||||
// name: 'Admin | Streams',
|
||||
// path: 'streams',
|
||||
@@ -218,6 +235,11 @@ const routes = [
|
||||
name: 'Admin | Settings',
|
||||
path: 'settings',
|
||||
component: () => import('@/views/admin/AdminSettings.vue')
|
||||
},
|
||||
{
|
||||
name: 'Admin | Invites',
|
||||
path: 'invites',
|
||||
component: () => import('@/views/admin/AdminInvites.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -253,7 +275,7 @@ const router = new VueRouter({
|
||||
mode: 'history',
|
||||
// base: process.env.BASE_URL,
|
||||
routes,
|
||||
scrollBehavior (to, from, savedPosition) {
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
return { x: 0, y: 0 }
|
||||
}
|
||||
})
|
||||
|
||||
+46
-24
@@ -55,12 +55,9 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/profile" v-if="user" style="height: 59px">
|
||||
<v-list-item v-if="user" link to="/profile" style="height: 59px">
|
||||
<v-list-item-icon>
|
||||
<v-avatar size="25">
|
||||
<v-img v-if="user.avatar" :src="user.avatar" />
|
||||
<v-img v-else :src="`https://robohash.org/` + user.id + `.png?size=38x38`" />
|
||||
</v-avatar>
|
||||
<user-avatar-icon :size="24" :avatar="user.avatar" :seed="user.id"></user-avatar-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
@@ -74,7 +71,7 @@
|
||||
<v-list-item v-if="serverInfo">
|
||||
<v-list-item-icon>
|
||||
<v-icon
|
||||
v-if="serverInfo && isDevServer"
|
||||
v-if="isDevServer"
|
||||
v-tooltip="`This is a test server and should not be used in production!`"
|
||||
color="red"
|
||||
>
|
||||
@@ -87,12 +84,14 @@
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ serverInfo.version }}
|
||||
</v-list-item-subtitle>
|
||||
<div class="caption">This is a test server and should not be used in production!</div>
|
||||
<div class="caption">
|
||||
{{ serverInfo.description }}
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-list dense>
|
||||
<v-list-item
|
||||
link
|
||||
@@ -109,7 +108,7 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link @click="signOut()" color="primary" v-if="user">
|
||||
<v-list-item v-if="user" link color="primary" @click="signOut()">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-account-off</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -118,7 +117,7 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/admin" color="primary" v-if="user && user.role === 'server:admin'">
|
||||
<v-list-item v-if="user && user.role === 'server:admin'" link to="/admin" color="primary">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-cog</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -140,22 +139,22 @@
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-bottom-navigation fixed xxx-hide-on-scroll class="hidden-sm-and-up elevation-20">
|
||||
<v-btn color="primary" text to="/" style="height: 100%;">
|
||||
<v-btn color="primary" text to="/" style="height: 100%">
|
||||
<span>Feed</span>
|
||||
<v-icon>mdi-clock-fast</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn color="primary" text to="/streams" style="height: 100%;">
|
||||
<v-btn color="primary" text to="/streams" style="height: 100%">
|
||||
<span>Streams</span>
|
||||
<v-icon>mdi-folder</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn color="primary" text to="/profile" style="height: 100%;">
|
||||
<v-btn color="primary" text to="/profile" style="height: 100%">
|
||||
<span>Profile</span>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn text @click="bottomSheet = true" style="height: 100%;">
|
||||
<v-btn text style="height: 100%" @click="bottomSheet = true">
|
||||
<span>More</span>
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
@@ -198,7 +197,7 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link @click="signOut()" color="primary" v-if="user">
|
||||
<v-list-item v-if="user" link color="primary" @click="signOut()">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-account-off</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -207,7 +206,7 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/admin" color="primary" v-if="user && user.role === 'server:admin'">
|
||||
<v-list-item v-if="user && user.role === 'server:admin'" link to="/admin" color="primary">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="ml-1">mdi-cog</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -252,17 +251,32 @@
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
</v-main>
|
||||
<v-snackbar
|
||||
v-model="streamSnackbar"
|
||||
rounded="pill"
|
||||
:timeout="10000"
|
||||
style="z-index: 10000"
|
||||
:color="`${$vuetify.theme.dark ? 'primary' : 'primary'}`"
|
||||
>
|
||||
<span v-if="streamSnackbarInfo.sharedBy">You have been granted access to a new stream!</span>
|
||||
<span v-else>New stream created!</span>
|
||||
<template #action="{ attrs }">
|
||||
<v-btn color="white" text v-bind="attrs" @click="goToStreamAndCloseSnackbar()">View</v-btn>
|
||||
<v-btn color="pink" icon v-bind="attrs" @click="streamSnackbar = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-app>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import { signOut } from '@/auth-helpers'
|
||||
import userQuery from '../graphql/user.gql'
|
||||
import SearchBar from '../components/SearchBar'
|
||||
import StreamInviteDialog from '../components/dialogs/StreamInviteDialog'
|
||||
import UserAvatarIcon from '@/components/UserAvatarIcon'
|
||||
|
||||
export default {
|
||||
components: { SearchBar, StreamInviteDialog },
|
||||
components: { UserAvatarIcon },
|
||||
data() {
|
||||
return {
|
||||
streamSnackbar: false,
|
||||
@@ -298,6 +312,7 @@ export default {
|
||||
}
|
||||
`,
|
||||
result(streamInfo) {
|
||||
console.log(streamInfo)
|
||||
if (!streamInfo.data.userStreamAdded) return
|
||||
this.streamSnackbar = true
|
||||
this.streamSnackbarInfo = streamInfo.data.userStreamAdded
|
||||
@@ -308,11 +323,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.bottomSheet = false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
background() {
|
||||
let theme = this.$vuetify.theme.dark ? 'dark' : 'light'
|
||||
@@ -325,6 +335,14 @@ export default {
|
||||
return localStorage.getItem('uuid') !== null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route(to) {
|
||||
this.bottomSheet = false
|
||||
// close the snackbar if it's a stream create event in this window
|
||||
if (to.params.streamId === this.streamSnackbarInfo.id) this.streamSnackbar = false
|
||||
this.bottomSheet = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
signOut() {
|
||||
signOut()
|
||||
@@ -335,6 +353,10 @@ export default {
|
||||
},
|
||||
showStreamInviteDialog() {
|
||||
this.$refs.streamInviteDialog.show()
|
||||
},
|
||||
goToStreamAndCloseSnackbar() {
|
||||
this.streamSnackbar = false
|
||||
this.$router.push(`/streams/${this.streamSnackbarInfo.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,10 @@
|
||||
pr-0
|
||||
>
|
||||
<v-navigation-drawer
|
||||
v-model="streamNav"
|
||||
app
|
||||
fixed
|
||||
:permanent="streamNav && !$vuetify.breakpoint.smAndDown"
|
||||
v-model="streamNav"
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
|
||||
>
|
||||
<main-nav-actions :open-new-stream="newStreamDialog" />
|
||||
@@ -51,12 +51,12 @@
|
||||
<v-subheader class="ml-2">Your latest commits:</v-subheader>
|
||||
<v-list-item
|
||||
v-for="(commit, i) in userCommits.commits.items"
|
||||
v-if="commit"
|
||||
:key="i"
|
||||
v-tooltip="`In stream '${commit.streamName}'`"
|
||||
:to="`streams/${commit.streamId}/${
|
||||
commit.branchName === 'globals' ? 'globals' : 'commits'
|
||||
}/${commit.id}`"
|
||||
v-if="commit"
|
||||
v-tooltip="`In stream '${commit.streamName}'`"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
@@ -84,12 +84,8 @@
|
||||
Streams
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items style="margin-right: -18px" v-if="$vuetify.breakpoint.smAndDown">
|
||||
<v-btn
|
||||
color="primary"
|
||||
depressed
|
||||
@click="newStreamDialog++"
|
||||
>
|
||||
<v-toolbar-items v-if="$vuetify.breakpoint.smAndDown" style="margin-right: -18px">
|
||||
<v-btn color="primary" depressed @click="newStreamDialog++">
|
||||
<v-icon>mdi-plus-box</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
@@ -107,7 +103,7 @@
|
||||
<div v-if="$apollo.loading" class="my-5"></div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" v-else-if="streams && streams.items && streams.items.length > 0">
|
||||
<v-col v-else-if="streams && streams.items && streams.items.length > 0" cols="12">
|
||||
<v-row :class="`${$vuetify.breakpoint.xsOnly ? '' : 'pl-2'}`">
|
||||
<v-col
|
||||
v-for="(stream, i) in streams.items"
|
||||
@@ -137,7 +133,7 @@
|
||||
here.
|
||||
</p>
|
||||
|
||||
<template v-slot:actions>
|
||||
<template #actions>
|
||||
<v-list rounded class="transparent">
|
||||
<v-list-item link class="primary mb-4" dark @click="newStreamDialog++">
|
||||
<v-list-item-icon>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<template>
|
||||
<v-container :style="`${ !$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" fluid pt-4 pr-0>
|
||||
<v-container
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`"
|
||||
fluid
|
||||
pt-4
|
||||
pr-0
|
||||
>
|
||||
<v-navigation-drawer
|
||||
v-model="activityNav"
|
||||
app
|
||||
fixed
|
||||
:permanent="activityNav && !$vuetify.breakpoint.smAndDown"
|
||||
v-model="activityNav"
|
||||
:style="`${ !$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
|
||||
>
|
||||
<main-nav-actions :open-new-stream="newStreamDialog" />
|
||||
|
||||
@@ -13,9 +18,9 @@
|
||||
<v-subheader class="mt-3 ml-2">Recently updated streams</v-subheader>
|
||||
<v-list-item
|
||||
v-for="(s, i) in streams.items"
|
||||
v-if="streams.items"
|
||||
:key="i"
|
||||
:to="'streams/' + s.id"
|
||||
v-if="streams.items"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
@@ -32,16 +37,14 @@
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-app-bar app :style="`${ !$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" flat>
|
||||
<v-app-bar-nav-icon
|
||||
@click="activityNav = !activityNav"
|
||||
></v-app-bar-nav-icon>
|
||||
<v-app-bar app :style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" flat>
|
||||
<v-app-bar-nav-icon @click="activityNav = !activityNav"></v-app-bar-nav-icon>
|
||||
<v-toolbar-title class="space-grotesk pl-0">
|
||||
<v-icon class="hidden-xs-only">mdi-clock-fast</v-icon>
|
||||
Feed
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items style="margin-right: -18px;" v-if="$vuetify.breakpoint.smAndDown ">
|
||||
<v-toolbar-items v-if="$vuetify.breakpoint.smAndDown" style="margin-right: -18px">
|
||||
<v-btn color="primary" depressed @click="newStreamDialog++">
|
||||
<v-icon>mdi-plus-box</v-icon>
|
||||
</v-btn>
|
||||
@@ -59,7 +62,13 @@
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" lg="8" v-else-if="timeline && timeline.items.length > 0" class="pr-2" :style="`${$vuetify.breakpoint.xsOnly ? 'margin-left: -20px;' :''}`">
|
||||
<v-col
|
||||
v-else-if="timeline && timeline.items.length > 0"
|
||||
cols="12"
|
||||
lg="8"
|
||||
class="pr-2"
|
||||
:style="`${$vuetify.breakpoint.xsOnly ? 'margin-left: -20px;' : ''}`"
|
||||
>
|
||||
<div>
|
||||
<div v-if="timeline" key="activity-list">
|
||||
<v-timeline align-top dense>
|
||||
@@ -83,20 +92,15 @@
|
||||
</v-col>
|
||||
<v-col v-else cols="12">
|
||||
<no-data-placeholder v-if="quickUser">
|
||||
<h2>Welcome {{quickUser.name.split(' ')[0]}}!</h2>
|
||||
<h2>Welcome {{ quickUser.name.split(' ')[0] }}!</h2>
|
||||
<p class="caption">
|
||||
Once you will create a stream and start sending some data, your activity will show up
|
||||
here.
|
||||
</p>
|
||||
|
||||
<template v-slot:actions>
|
||||
<template #actions>
|
||||
<v-list rounded class="transparent">
|
||||
<v-list-item
|
||||
link
|
||||
class="primary mb-4"
|
||||
dark
|
||||
@click="newStreamDialog++"
|
||||
>
|
||||
<v-list-item link class="primary mb-4" dark @click="newStreamDialog++">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-plus-box</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -113,11 +117,11 @@
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-show="$vuetify.breakpoint.lgAndUp"
|
||||
v-if="timeline && timeline.items.length > 0"
|
||||
cols="12"
|
||||
lg="4"
|
||||
v-show="$vuetify.breakpoint.lgAndUp"
|
||||
class="mt-7"
|
||||
v-if="timeline && timeline.items.length > 0"
|
||||
>
|
||||
<latest-blogposts></latest-blogposts>
|
||||
<v-card rounded="lg" class="mt-2">
|
||||
@@ -170,18 +174,16 @@ export default {
|
||||
activityNav: true
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
mounted() {
|
||||
setTimeout(
|
||||
function () {
|
||||
this.activityNav = !this.$vuetify.breakpoint.smAndDown
|
||||
}.bind(this),
|
||||
10
|
||||
)
|
||||
},
|
||||
apollo: {
|
||||
quickUser: {
|
||||
query: gql`query { quickUser: user { id name } } `
|
||||
query: gql`
|
||||
query {
|
||||
quickUser: user {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
},
|
||||
timeline: {
|
||||
query: gql`
|
||||
@@ -217,7 +219,7 @@ export default {
|
||||
prefetch: true,
|
||||
query: gql`
|
||||
query {
|
||||
streams (limit: 10) {
|
||||
streams(limit: 10) {
|
||||
items {
|
||||
id
|
||||
name
|
||||
@@ -228,9 +230,25 @@ export default {
|
||||
`
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
watch: {
|
||||
timeline(val) {
|
||||
if (val.totalCount === 0 && !localStorage.getItem('onboarding')) {
|
||||
this.$router.push('/onboarding')
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(
|
||||
function () {
|
||||
this.activityNav = !this.$vuetify.breakpoint.smAndDown
|
||||
}.bind(this),
|
||||
10
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
groupSimilarActivities(data) {
|
||||
if(!data) return
|
||||
if (!data) return
|
||||
let groupedTimeline = data.user.timeline.items.reduce(function (prev, curr) {
|
||||
//first item
|
||||
if (!prev.length) {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<template lang="html">
|
||||
|
||||
<v-container v-if="$apollo.loading" fluid>
|
||||
<v-skeleton-loader type="article"></v-skeleton-loader>
|
||||
</v-container>
|
||||
|
||||
<v-container v-else-if="isAdmin" :class="`${$vuetify.breakpoint.xsOnly ? 'pl-0' : ''}`">
|
||||
<v-navigation-drawer
|
||||
v-model="adminNav"
|
||||
app
|
||||
fixed
|
||||
:permanent="adminNav && !$vuetify.breakpoint.smAndDown"
|
||||
v-model="adminNav"
|
||||
:style="`${ !$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
|
||||
>
|
||||
<v-app-bar style="position: absolute; top: 0; width: 100%; z-index: 90" elevation="0">
|
||||
<v-toolbar-title>Server Admin</v-toolbar-title>
|
||||
@@ -40,29 +39,54 @@
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/admin/users">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="mt-1">mdi-account-group</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Users</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">Edit server user details.</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/admin/invites">
|
||||
<v-list-item-icon>
|
||||
<v-icon small class="mt-1">mdi-account-multiple-plus-outline</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Server invites</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">Manage server invitations.</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-app-bar app :style="`${ !$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" flat v-if="!adminNav">
|
||||
<v-app-bar-nav-icon @click="adminNav = !adminNav" v-if="!adminNav"/>
|
||||
<v-toolbar-title v-if="!adminNav">
|
||||
Server Admin
|
||||
</v-toolbar-title>
|
||||
<v-app-bar
|
||||
v-if="!adminNav"
|
||||
app
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`"
|
||||
flat
|
||||
>
|
||||
<v-app-bar-nav-icon v-if="!adminNav" @click="adminNav = !adminNav" />
|
||||
<v-toolbar-title v-if="!adminNav">Server Admin</v-toolbar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-container :style="`${ !$vuetify.breakpoint.xsOnly ? 'padding-left: 56px;' : ''} max-width: 1024px;`" fluid pt-4 pr-0>
|
||||
<v-container
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px;' : ''} max-width: 1024px;`"
|
||||
fluid
|
||||
pt-4
|
||||
pr-0
|
||||
>
|
||||
<transition name="fade">
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
</v-container>
|
||||
</v-container>
|
||||
|
||||
<v-container v-else-if="!isAdmin">
|
||||
<error-placeholder error-type="access">
|
||||
<h2>Only server admins have access to this section.</h2>
|
||||
</error-placeholder>
|
||||
</v-container>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -70,7 +94,7 @@ import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'AdminPanel',
|
||||
components:{
|
||||
components: {
|
||||
ErrorPlaceholder: () => import('@/components/ErrorPlaceholder')
|
||||
},
|
||||
data() {
|
||||
@@ -98,5 +122,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>Send invites to multiple adresses</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<v-alert v-model="success" prominent timeout="3000" dismissible type="success">
|
||||
Great! All invites were sent.
|
||||
</v-alert>
|
||||
<v-alert v-show="errors.length !== 0" prominent dismissible type="error">
|
||||
<p>Invite send failed for adresses:</p>
|
||||
<ul>
|
||||
<li v-for="error in errors" :key="error.email">{{ error.email }}: {{ error.reason }}</li>
|
||||
</ul>
|
||||
</v-alert>
|
||||
<v-alert
|
||||
v-show="errors.length !== 0 && sentToEmails.length !== 0"
|
||||
prominent
|
||||
timeout="3000"
|
||||
dismissible
|
||||
type="success"
|
||||
>
|
||||
<p>Invite sent to: {{ sentToEmails.join(', ') }}</p>
|
||||
</v-alert>
|
||||
<v-form v-model="valid" @submit.prevent="submit">
|
||||
<v-textarea
|
||||
v-model="invitation"
|
||||
label="Invitation message"
|
||||
rounded
|
||||
filled
|
||||
auto-grow
|
||||
:rules="validation.messageRules"
|
||||
></v-textarea>
|
||||
<v-combobox
|
||||
v-model="chips"
|
||||
:search-input.sync="emails"
|
||||
placeholder="Type emails separated by commas or paste the content of a .csv"
|
||||
deletable-chips
|
||||
append-icon=""
|
||||
filled
|
||||
rounded
|
||||
flat
|
||||
type="email"
|
||||
class="lighten-2"
|
||||
:error-messages="inputErrors"
|
||||
multiple
|
||||
append-outer-icon="mdi-close"
|
||||
@keydown="keyDownHandler"
|
||||
@blur="validateAndCreateChips"
|
||||
@paste="validateAndCreateChips"
|
||||
@click:append-outer="chips = []"
|
||||
>
|
||||
<template #selection="data">
|
||||
<v-chip
|
||||
v-if="data.item"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
@click:close="remove(data.item)"
|
||||
>
|
||||
{{ data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
<p v-if="!selectedStream">Optionaly invite users to stream.</p>
|
||||
<stream-search-bar
|
||||
v-if="!selectedStream"
|
||||
:gotostreamonclick="false"
|
||||
class="py-3"
|
||||
@select="setStream"
|
||||
/>
|
||||
<v-alert v-else text dense type="info" dismissible @input="dismiss">
|
||||
They will be invited to be collaborators on {{ selectedStream.name }} stream.
|
||||
</v-alert>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn :disabled="!submitable" color="primary" type="submit">Invite</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-overlay absolute :value="submitting">
|
||||
<v-progress-circular :width="1.5" indeterminate></v-progress-circular>
|
||||
</v-overlay>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import DOMPurify from 'dompurify'
|
||||
import StreamSearchBar from '@/components/SearchBar'
|
||||
export default {
|
||||
components: { StreamSearchBar },
|
||||
data() {
|
||||
return {
|
||||
valid: false,
|
||||
success: false,
|
||||
showError: false,
|
||||
errors: [],
|
||||
sentToEmails: [],
|
||||
submitting: false,
|
||||
invitation: '',
|
||||
emails: '',
|
||||
chips: [],
|
||||
inputErrors: [],
|
||||
selectedStream: null,
|
||||
validation: {
|
||||
messageRules: [
|
||||
(v) => {
|
||||
if (v.length >= 1024) return 'Message too long!'
|
||||
return true
|
||||
},
|
||||
(v) => {
|
||||
let pure = DOMPurify.sanitize(v)
|
||||
if (pure !== v) return 'No crazy hacks please.'
|
||||
else return true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
submitable() {
|
||||
return this.chips && this.chips.length !== 0
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
user: {
|
||||
query: gql`
|
||||
query {
|
||||
user {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
prefetch: true
|
||||
},
|
||||
serverInfo: {
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
canonicalUrl
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setStream(stream) {
|
||||
this.selectedStream = stream
|
||||
},
|
||||
dismiss() {
|
||||
this.selectedStream = null
|
||||
},
|
||||
remove(item) {
|
||||
this.chips.splice(this.chips.indexOf(item), 1)
|
||||
},
|
||||
validateEmail(email) {
|
||||
const re = /^\S+@\S+\.\S+$/
|
||||
return re.test(email)
|
||||
},
|
||||
keyDownHandler(val) {
|
||||
if (!(val.key === ' ' || val.key === ',' || val.key === 'Enter')) return
|
||||
this.validateAndCreateChips()
|
||||
},
|
||||
validateAndCreateChips() {
|
||||
this.inputErrors = []
|
||||
if (!this.emails || this.emails === '') return
|
||||
let splitEmails = this.emails.split(/[ ,]+/)
|
||||
for (let email of splitEmails) {
|
||||
let valid = this.validateEmail(email) && this.chips.indexOf(email) === -1
|
||||
if (valid) {
|
||||
this.chips.push(email)
|
||||
} else {
|
||||
this.inputErrors.push('Invalid email')
|
||||
}
|
||||
}
|
||||
this.emails = ''
|
||||
},
|
||||
createInviteMessage() {
|
||||
let message =
|
||||
`You have been invited to a Speckle server: ${this.serverInfo.name} ` +
|
||||
`by ${this.user.name}. Visit ${this.serverInfo.canonicalUrl} to register.`
|
||||
return this.invitation || message
|
||||
},
|
||||
async submit() {
|
||||
this.submitting = true
|
||||
this.errors = []
|
||||
this.sentToEmails = []
|
||||
for (let chip of this.chips) {
|
||||
if (!chip || chip.length === 0) continue
|
||||
try {
|
||||
await this.sendInvite(chip, this.createInviteMessage(), this.selectedStream?.id)
|
||||
this.sentToEmails.push(chip)
|
||||
} catch (err) {
|
||||
this.errors.push({ email: chip, reason: err.graphQLErrors[0].message })
|
||||
}
|
||||
}
|
||||
|
||||
this.submitting = false
|
||||
if (this.errors.length === 0) {
|
||||
this.success = true
|
||||
this.chips = []
|
||||
this.dismiss()
|
||||
}
|
||||
},
|
||||
async sendInvite(email, message, streamId) {
|
||||
let input = {
|
||||
email: email,
|
||||
message: message
|
||||
}
|
||||
|
||||
let query = gql`
|
||||
mutation($input: ${streamId ? 'StreamInviteCreateInput!' : 'ServerInviteCreateInput!'}) {
|
||||
${streamId ? 'streamInviteCreate' : 'serverInviteCreate'}(input: $input)
|
||||
}
|
||||
`
|
||||
if (streamId) {
|
||||
input.streamId = streamId
|
||||
}
|
||||
|
||||
await this.$apollo.mutate({
|
||||
mutation: query,
|
||||
variables: {
|
||||
input: input
|
||||
}
|
||||
})
|
||||
|
||||
this.$matomo && this.$matomo.trackEvent('invite', 'server')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,41 +1,41 @@
|
||||
<template>
|
||||
<div id="admin-settings">
|
||||
<v-card rounded="lg" v-if="serverInfo">
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
|
||||
<v-toolbar-title>{{ serverInfo.name }}</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<div key="viewPanel">
|
||||
<div class="d-flex align-center mb-2" v-for="(value, name) in serverDetails" :key="name">
|
||||
<div class="flex-grow-1">
|
||||
<div v-if="value.type == 'boolean'">
|
||||
<p class="mt-2">{{value.label}}</p>
|
||||
<v-switch
|
||||
inset
|
||||
persistent-hint
|
||||
<v-card v-if="serverInfo" rounded="lg">
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
|
||||
<v-toolbar-title>{{ serverInfo.name }}</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<div key="viewPanel">
|
||||
<div v-for="(value, name) in serverDetails" :key="name" class="d-flex align-center mb-2">
|
||||
<div class="flex-grow-1">
|
||||
<div v-if="value.type == 'boolean'">
|
||||
<p class="mt-2">{{ value.label }}</p>
|
||||
<v-switch
|
||||
v-model="serverModifications[name]"
|
||||
inset
|
||||
persistent-hint
|
||||
class="pa-1 ma-1 caption"
|
||||
>
|
||||
<template #label>
|
||||
<span class="caption">{{ value.hint }}</span>
|
||||
</template>
|
||||
</v-switch>
|
||||
</div>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="serverModifications[name]"
|
||||
class="pa-1 ma-1 caption"
|
||||
>
|
||||
<template v-slot:label>
|
||||
<span class="caption">{{ value.hint }}</span>
|
||||
</template>
|
||||
</v-switch>
|
||||
persistent-hint
|
||||
:hint="value.hint"
|
||||
class="ma-0 body-2"
|
||||
></v-text-field>
|
||||
</div>
|
||||
<v-text-field
|
||||
persistent-hint
|
||||
v-else
|
||||
:hint="value.hint"
|
||||
v-model="serverModifications[name]"
|
||||
class="ma-0 body-2"
|
||||
></v-text-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn block color="primary" @click="saveEdit" :loading="loading">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn block color="primary" :loading="loading" @click="saveEdit">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,155 +1,242 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card title="Users">
|
||||
<template slot="menu">
|
||||
<span class="caption mr-2">Showing XX of YYY users.</span>
|
||||
<v-menu offset-y left class="rounded-circle">
|
||||
<template #activator="{ attrs, on }">
|
||||
<v-btn v-bind="attrs" v-on="on" icon outlined small color="primary" class="mr-2">
|
||||
<v-icon small>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list class="rounded-circle">
|
||||
<v-list-item>Hi</v-list-item>
|
||||
<v-list-item>Hi</v-list-item>
|
||||
<v-list-item>Hi</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-card>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>
|
||||
Server Users Admin
|
||||
<span v-if="users">(found {{ users.totalCount }} users)</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
class="mx-4 mt-4"
|
||||
:prepend-inner-icon="'mdi-magnify'"
|
||||
:loading="$apollo.loading"
|
||||
label="Search users"
|
||||
type="text"
|
||||
single-line
|
||||
clearable
|
||||
rounded
|
||||
filled
|
||||
dense
|
||||
></v-text-field>
|
||||
<v-list v-if="!$apollo.loading" rounded>
|
||||
<v-list-item-group v-if="users.totalCount > 0" color="primary">
|
||||
<v-list-item v-for="user in users.items" :key="user.id">
|
||||
<v-list-item-avatar :size="55">
|
||||
<user-avatar-icon
|
||||
class="ml-n2"
|
||||
:avatar="user.avatar"
|
||||
:seed="user.id"
|
||||
:size="50"
|
||||
></user-avatar-icon>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ user.name }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-btn icon outlined small color="primary" class="mr-2">
|
||||
<v-icon small>mdi-filter</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon outlined small color="primary">
|
||||
<v-icon small>mdi-sort</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<user-list-item v-for="user in users" :key="user.id" :admin="user" :widgets="widgets(user)">
|
||||
<v-menu offset-y left rounded>
|
||||
<template v-slot:activator="{ attrs, on }">
|
||||
<v-btn icon small v-bind="attrs" v-on="on" class="ml-2">
|
||||
<v-icon small>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list nav dense>
|
||||
<v-tooltip
|
||||
left
|
||||
max-width="200pt"
|
||||
open-delay="500"
|
||||
v-for="opt in menuOptions"
|
||||
:key="opt.text"
|
||||
:disabled="!opt.hint"
|
||||
>
|
||||
<template v-slot:activator="{ attrs, on }">
|
||||
<v-list-item
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
link
|
||||
@click="opt.action ? opt.action(user) : null"
|
||||
>
|
||||
<v-list-item-icon class="mr-3">
|
||||
<v-icon
|
||||
small
|
||||
v-text="opt.icon"
|
||||
:class="`${opt.color || 'dark'}--text`"
|
||||
></v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title
|
||||
v-text="opt.text"
|
||||
:class="`${opt.color || 'dark'}--text`"
|
||||
></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
{{ opt.hint }}
|
||||
</v-tooltip>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</user-list-item>
|
||||
|
||||
<div class="text-center subtitle-2 pt-3">
|
||||
<v-btn :loading="loading" text width="100%" color="primary" @click="loadMoreUsers">
|
||||
Load more
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
<span class="caption">
|
||||
<v-icon x-small>mdi-email-outline</v-icon>
|
||||
{{ user.email }}
|
||||
</span>
|
||||
<span v-if="user.company" class="caption">
|
||||
<v-icon x-small>mdi-domain</v-icon>
|
||||
{{ user.company }}
|
||||
</span>
|
||||
<span v-else class="caption">
|
||||
<v-icon x-small>mdi-help-circle</v-icon>
|
||||
No company info
|
||||
</span>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-select
|
||||
:value="user.role"
|
||||
:items="availableRoles"
|
||||
label="user role"
|
||||
@change="changeUserRole(user, ...arguments)"
|
||||
></v-select>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
<div class="text-center">
|
||||
<v-pagination
|
||||
v-model="currentPage"
|
||||
:length="numberOfPages"
|
||||
:total-visible="7"
|
||||
circle
|
||||
></v-pagination>
|
||||
</div>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
<v-skeleton-loader v-else class="mx-auto" type="card"></v-skeleton-loader>
|
||||
<v-dialog v-model="showConfirmDialog" persistent max-width="600px">
|
||||
<v-card v-if="showConfirmDialog">
|
||||
<v-toolbar flat class="mb-6">
|
||||
<v-toolbar-title>Confirm user role change</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<v-alert v-if="newRole === 'server:admin'" type="error">
|
||||
Make sure you trust {{ manipulatedUser.name }}!
|
||||
<br />
|
||||
An admin on the server has access to every resource.
|
||||
</v-alert>
|
||||
You are changing {{ manipulatedUser.name }}'s server access role from
|
||||
{{ roleLookupTable[manipulatedUser.role] }} to {{ roleLookupTable[newRole] }}.
|
||||
<br />
|
||||
Proceed?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" text @click="cancelRoleChange">Cancel</v-btn>
|
||||
<v-btn color="primary" text @click="proceedRoleChange">Proceed</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserListItem from '@/components/admin/UserListItem'
|
||||
import gql from 'graphql-tag'
|
||||
import UserAvatarIcon from '@/components/UserAvatarIcon'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
export default {
|
||||
name: 'AdminUsers',
|
||||
components: { UserListItem },
|
||||
methods: {
|
||||
viewProfile(user) {
|
||||
console.log('requesting profile for user with id ', user.id)
|
||||
},
|
||||
widgets(user) {
|
||||
return [
|
||||
{
|
||||
icon: 'mdi-eye-outline',
|
||||
hint: 'Last seen',
|
||||
value: '< 1 day',
|
||||
type: 'text',
|
||||
color: null
|
||||
},
|
||||
{
|
||||
icon: 'mdi-cloud-outline',
|
||||
hint: 'Streams',
|
||||
value: user.streams.totalCount,
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
icon: 'mdi-cloud-upload-outline',
|
||||
hint: 'Commits',
|
||||
value: user.commits.totalCount,
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
icon: 'mdi-calendar-outline',
|
||||
hint: 'Joined in',
|
||||
value: 'Feb/21',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
icon: 'mdi-decagram-outline',
|
||||
hint: 'Badges',
|
||||
value: 55
|
||||
}
|
||||
]
|
||||
},
|
||||
loadMoreUsers() {
|
||||
console.log('requested more users!')
|
||||
this.loading = true
|
||||
those
|
||||
setTimeout(() => {
|
||||
this.loading = false
|
||||
}, 700)
|
||||
}
|
||||
name: 'UserAdmin',
|
||||
components: { UserAvatarIcon },
|
||||
props: {
|
||||
limit: { type: [Number, String], required: false, default: 10 },
|
||||
page: { type: [Number, String], required: false, default: 1 },
|
||||
q: { type: String, required: false, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
menuOptions: [
|
||||
{
|
||||
text: 'View profile',
|
||||
icon: 'mdi-account',
|
||||
action: this.viewProfile
|
||||
roleLookupTable: {
|
||||
'server:user': 'User',
|
||||
'server:admin': 'Admin'
|
||||
// 'server:archived_user': 'Archived'
|
||||
},
|
||||
users: {
|
||||
items: [],
|
||||
totalCount: 0
|
||||
},
|
||||
currentPage: 1,
|
||||
searchQuery: null,
|
||||
showConfirmDialog: false,
|
||||
manipulatedUser: null,
|
||||
newRole: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
queryLimit() {
|
||||
return parseInt(this.limit)
|
||||
},
|
||||
queryOffset() {
|
||||
return (this.page - 1) * this.queryLimit
|
||||
},
|
||||
numberOfPages() {
|
||||
return Math.ceil(this.users.totalCount / this.limit)
|
||||
},
|
||||
availableRoles() {
|
||||
let roleItems = []
|
||||
for (let role in this.roleLookupTable) {
|
||||
roleItems.push({ text: this.roleLookupTable[role], value: role })
|
||||
}
|
||||
return roleItems
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentPage: function (newPage) {
|
||||
this.paginateNext(newPage)
|
||||
},
|
||||
searchQuery: debounce(function (newQuery) {
|
||||
this.applySearch(newQuery)
|
||||
}, 1000)
|
||||
},
|
||||
methods: {
|
||||
changeUserRole(user, args) {
|
||||
this.manipulatedUser = user
|
||||
this.newRole = args
|
||||
console.log(user.role)
|
||||
console.log(this.newRole)
|
||||
|
||||
this.showConfirmDialog = true
|
||||
},
|
||||
resetManipulatedUser() {
|
||||
this.manipulatedUser = null
|
||||
this.newRole = null
|
||||
},
|
||||
cancelRoleChange() {
|
||||
this.showConfirmDialog = false
|
||||
this.$apollo.queries.users.refetch()
|
||||
this.resetManipulatedUser()
|
||||
},
|
||||
async proceedRoleChange() {
|
||||
await this.updateUserRole(this.manipulatedUser.id, this.newRole)
|
||||
this.resetManipulatedUser()
|
||||
this.showConfirmDialog = false
|
||||
},
|
||||
async updateUserRole(userId, newRole) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($userId: String!, $newRole: String!) {
|
||||
userRoleChange(userRoleInput: { id: $userId, role: $newRole })
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
userId,
|
||||
newRole
|
||||
},
|
||||
{
|
||||
text: 'Delete user',
|
||||
icon: 'mdi-delete',
|
||||
color: 'error',
|
||||
hint: "Removes this user's admin privileges. This will not delete the user's account."
|
||||
update: () => {
|
||||
this.$apollo.queries.users.refetch()
|
||||
},
|
||||
error: (err) => {
|
||||
console.log(err)
|
||||
}
|
||||
],
|
||||
users:[],
|
||||
})
|
||||
},
|
||||
paginateNext(newPage) {
|
||||
this.$router.push(this._prepareRoute(newPage, this.limit, this.searchQuery))
|
||||
},
|
||||
applySearch(searchQuery) {
|
||||
this.$router.push(this._prepareRoute(1, this.limit, searchQuery))
|
||||
},
|
||||
_prepareRoute(page, limit, query) {
|
||||
let newRoute = `users?page=${page}&limit=${limit}`
|
||||
if (query) newRoute = `${newRoute}&q=${query}`
|
||||
return newRoute
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
users: {
|
||||
query: gql`
|
||||
query Users($limit: Int, $offset: Int, $query: String) {
|
||||
users(limit: $limit, offset: $offset, query: $query) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
suuid
|
||||
email
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
profiles
|
||||
role
|
||||
authorizedApps {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
limit: this.queryLimit,
|
||||
offset: this.queryOffset,
|
||||
query: this.q
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card title="Users">
|
||||
<template slot="menu">
|
||||
<span class="caption mr-2">Showing XX of YYY users.</span>
|
||||
<v-menu offset-y left class="rounded-circle">
|
||||
<template #activator="{ attrs, on }">
|
||||
<v-btn v-bind="attrs" v-on="on" icon outlined small color="primary" class="mr-2">
|
||||
<v-icon small>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list class="rounded-circle">
|
||||
<v-list-item>Hi</v-list-item>
|
||||
<v-list-item>Hi</v-list-item>
|
||||
<v-list-item>Hi</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-btn icon outlined small color="primary" class="mr-2">
|
||||
<v-icon small>mdi-filter</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon outlined small color="primary">
|
||||
<v-icon small>mdi-sort</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<user-list-item v-for="user in users" :key="user.id" :admin="user" :widgets="widgets(user)">
|
||||
<v-menu offset-y left rounded>
|
||||
<template v-slot:activator="{ attrs, on }">
|
||||
<v-btn icon small v-bind="attrs" v-on="on" class="ml-2">
|
||||
<v-icon small>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list nav dense>
|
||||
<v-tooltip
|
||||
left
|
||||
max-width="200pt"
|
||||
open-delay="500"
|
||||
v-for="opt in menuOptions"
|
||||
:key="opt.text"
|
||||
:disabled="!opt.hint"
|
||||
>
|
||||
<template v-slot:activator="{ attrs, on }">
|
||||
<v-list-item
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
link
|
||||
@click="opt.action ? opt.action(user) : null"
|
||||
>
|
||||
<v-list-item-icon class="mr-3">
|
||||
<v-icon
|
||||
small
|
||||
v-text="opt.icon"
|
||||
:class="`${opt.color || 'dark'}--text`"
|
||||
></v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title
|
||||
v-text="opt.text"
|
||||
:class="`${opt.color || 'dark'}--text`"
|
||||
></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
{{ opt.hint }}
|
||||
</v-tooltip>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</user-list-item>
|
||||
|
||||
<div class="text-center subtitle-2 pt-3">
|
||||
<v-btn :loading="loading" text width="100%" color="primary" @click="loadMoreUsers">
|
||||
Load more
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserListItem from '@/components/admin/UserListItem'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'AdminUsers',
|
||||
components: { UserListItem },
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
menuOptions: [
|
||||
{
|
||||
text: 'View profile',
|
||||
icon: 'mdi-account',
|
||||
action: this.viewProfile
|
||||
},
|
||||
{
|
||||
text: 'Delete user',
|
||||
icon: 'mdi-delete',
|
||||
color: 'error',
|
||||
hint: "Removes this user's admin privileges. This will not delete the user's account."
|
||||
}
|
||||
],
|
||||
users: []
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
users: {
|
||||
prefetch: true,
|
||||
query: gql`
|
||||
query Users($limit: Int, $offset: Int) {
|
||||
users(limit: $limit, offset: $offset) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
suuid
|
||||
email
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
profiles
|
||||
role
|
||||
authorizedApps {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
viewProfile(user) {
|
||||
console.log('requesting profile for user with id ', user.id)
|
||||
},
|
||||
widgets(user) {
|
||||
return [
|
||||
{
|
||||
icon: 'mdi-eye-outline',
|
||||
hint: 'Last seen',
|
||||
value: '< 1 day',
|
||||
type: 'text',
|
||||
color: null
|
||||
},
|
||||
{
|
||||
icon: 'mdi-cloud-outline',
|
||||
hint: 'Streams',
|
||||
value: user.streams.totalCount,
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
icon: 'mdi-cloud-upload-outline',
|
||||
hint: 'Commits',
|
||||
value: user.commits.totalCount,
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
icon: 'mdi-calendar-outline',
|
||||
hint: 'Joined in',
|
||||
value: 'Feb/21',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
icon: 'mdi-decagram-outline',
|
||||
hint: 'Badges',
|
||||
value: 55
|
||||
}
|
||||
]
|
||||
},
|
||||
loadMoreUsers() {
|
||||
console.log('requested more users!')
|
||||
this.loading = true
|
||||
setTimeout(() => {
|
||||
this.loading = false
|
||||
}, 700)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -18,13 +18,15 @@
|
||||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content class="">
|
||||
<p>
|
||||
<b>Author:</b>
|
||||
<p class="d-flex align-center">
|
||||
<b class="mr-1">Author:</b>
|
||||
{{ app.author.name }}
|
||||
<v-avatar class="ma-1 ml-2" color="grey lighten-3" :size="20">
|
||||
<v-img v-if="app.author.avatar" :src="app.author.avatar" />
|
||||
<v-img v-else :src="`https://robohash.org/` + app.author.id + `.png?size=40x40`" />
|
||||
</v-avatar>
|
||||
<user-avatar-icon
|
||||
:avatar="app.author.avatar"
|
||||
:seed="app.author.id"
|
||||
:size="20"
|
||||
class="ml-1"
|
||||
></user-avatar-icon>
|
||||
</p>
|
||||
<p>
|
||||
<b>Description:</b>
|
||||
@@ -65,10 +67,11 @@
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import UserAvatar from '../../components/UserAvatarAuthoriseApp'
|
||||
import UserAvatarIcon from '@/components/UserAvatarIcon'
|
||||
|
||||
export default {
|
||||
name: 'AuthorizeApp',
|
||||
components: { UserAvatar },
|
||||
components: { UserAvatar, UserAvatarIcon },
|
||||
apollo: {
|
||||
app: {
|
||||
query: gql`
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<span class="space-grotesk" style="max-width: 80%">{{ stream.branch.name }}</span>
|
||||
<span class="caption ml-2 mb-2 pb-2">{{ stream.branch.description }}</span>
|
||||
<v-chip
|
||||
v-tooltip="`Branch ${stream.branch.name} has ${stream.branch.commits.totalCount} commits`"
|
||||
class="ml-2 pl-2"
|
||||
small
|
||||
v-tooltip="`Branch ${stream.branch.name} has ${stream.branch.commits.totalCount} commits`"
|
||||
>
|
||||
<v-icon small>mdi-source-commit</v-icon>
|
||||
{{ stream.branch.commits.totalCount }}
|
||||
@@ -17,7 +17,6 @@
|
||||
</portal>
|
||||
<portal to="streamActionsBar">
|
||||
<v-btn
|
||||
elevation="0"
|
||||
v-if="
|
||||
loggedInUserId &&
|
||||
stream &&
|
||||
@@ -25,12 +24,13 @@
|
||||
stream.branch &&
|
||||
stream.branch.name !== 'main'
|
||||
"
|
||||
v-tooltip="'Edit branch'"
|
||||
elevation="0"
|
||||
color="primary"
|
||||
small
|
||||
rounded
|
||||
:fab="$vuetify.breakpoint.mdAndDown"
|
||||
dark
|
||||
v-tooltip="'Edit branch'"
|
||||
@click="editBranch()"
|
||||
>
|
||||
<v-icon small :class="`${$vuetify.breakpoint.mdAndDown ? '' : 'mr-2'}`">mdi-pencil</v-icon>
|
||||
@@ -44,11 +44,11 @@
|
||||
<v-col v-else-if="stream && stream.branch" cols="12" class="pa-0 ma-0">
|
||||
<branch-edit-dialog ref="editBranchDialog" />
|
||||
|
||||
<div style="height: 60vh" v-if="latestCommitObjectUrl">
|
||||
<div v-if="latestCommitObjectUrl" style="height: 60vh">
|
||||
<renderer :object-url="latestCommitObjectUrl" show-selection-helper />
|
||||
</div>
|
||||
|
||||
<v-list class="pa-0 ma-0" v-if="stream.branch.commits.items.length > 0">
|
||||
<v-list v-if="stream.branch.commits.items.length > 0" class="pa-0 ma-0">
|
||||
<list-item-commit
|
||||
:commit="latestCommit"
|
||||
:stream-id="streamId"
|
||||
@@ -66,19 +66,18 @@
|
||||
></list-item-commit>
|
||||
|
||||
<!-- TODO: pagination -->
|
||||
|
||||
</v-list>
|
||||
</v-col>
|
||||
|
||||
<no-data-placeholder
|
||||
v-if="!$apollo.loading && stream.branch && stream.branch.commits.totalCount === 0"
|
||||
>
|
||||
<h2 class="space-grotesk">Branch "{{stream.branch.name}}" has no commits.</h2>
|
||||
<h2 class="space-grotesk">Branch "{{ stream.branch.name }}" has no commits.</h2>
|
||||
</no-data-placeholder>
|
||||
</v-row>
|
||||
<error-placeholder
|
||||
error-type="404"
|
||||
v-if="!$apollo.loading && (error || stream.branch === null)"
|
||||
error-type="404"
|
||||
>
|
||||
<h2>{{ error || `Branch "${$route.params.branchName}" does not exist.` }}</h2>
|
||||
</error-placeholder>
|
||||
@@ -162,6 +161,10 @@ export default {
|
||||
else return null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$route.params.branchName === 'globals')
|
||||
this.$router.push(`/streams/${this.$route.params.streamId}/globals`)
|
||||
},
|
||||
methods: {
|
||||
editBranch() {
|
||||
this.$refs.editBranchDialog.open(this.stream.branch).then((dialog) => {
|
||||
@@ -181,11 +184,6 @@ export default {
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log(this.$route.params)
|
||||
if (this.$route.params.branchName === 'globals')
|
||||
this.$router.push(`/streams/${this.$route.params.streamId}/globals`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-container style="max-width: 768px;">
|
||||
<v-container style="max-width: 768px">
|
||||
<portal to="streamTitleBar">
|
||||
<div>
|
||||
<v-icon small class="mr-2 hidden-xs-only">mdi-account-multiple</v-icon>
|
||||
@@ -7,62 +7,64 @@
|
||||
</div>
|
||||
</portal>
|
||||
|
||||
<v-alert type="warning" v-if="stream.role !== 'stream:owner'">
|
||||
<v-alert v-if="stream.role !== 'stream:owner'" type="warning">
|
||||
Your permission level ({{ stream.role }}) is not high enough to edit this stream's
|
||||
collaborators.
|
||||
</v-alert>
|
||||
<v-card
|
||||
v-if="serverInfo"
|
||||
elevation="0"
|
||||
color="transparent"
|
||||
:class="`mb-4 py-4`"
|
||||
>
|
||||
<v-card v-if="serverInfo" elevation="0" color="transparent" :class="`mb-4 py-4`">
|
||||
<v-row align="stretch">
|
||||
<v-col cols="12" sm="4" v-for="role in roles" :key="role.name">
|
||||
<v-card rounded="lg" style="height: 100%" :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''} d-flex flex-column`">
|
||||
<v-toolbar style="flex:none;" flat>
|
||||
<v-col v-for="role in roles" :key="role.name" cols="12" sm="4">
|
||||
<v-card
|
||||
rounded="lg"
|
||||
style="height: 100%"
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''} d-flex flex-column`"
|
||||
>
|
||||
<v-toolbar style="flex: none" flat>
|
||||
<v-toolbar-title class="text-capitalize">
|
||||
{{ role.name.split(':')[1] }}s
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-badge inline :content="getRoleCount(role.name)" :color="`grey ${$vuetify.theme.dark ? 'darken-1' : 'lighten-1'}`"></v-badge>
|
||||
<v-badge
|
||||
inline
|
||||
:content="getRoleCount(role.name)"
|
||||
:color="`grey ${$vuetify.theme.dark ? 'darken-1' : 'lighten-1'}`"
|
||||
></v-badge>
|
||||
</v-toolbar>
|
||||
<v-card-text class="flex-grow-1">{{ role.description }}</v-card-text>
|
||||
<v-card-text class="mt-auto">
|
||||
<div v-if="role.name === 'stream:reviewer'" class="align-self-end">
|
||||
<user-avatar
|
||||
v-for="user in reviewers"
|
||||
:key="user.id"
|
||||
:id="user.id"
|
||||
:key="user.id"
|
||||
:avatar="user.avatar"
|
||||
:name="user.name"
|
||||
:size="30"
|
||||
/>
|
||||
<span v-if="reviewers.length===0">No users with this role.</span>
|
||||
<span v-if="reviewers.length === 0">No users with this role.</span>
|
||||
</div>
|
||||
<div v-if="role.name === 'stream:contributor'">
|
||||
<user-avatar
|
||||
v-for="user in contributors"
|
||||
:key="user.id"
|
||||
:id="user.id"
|
||||
:key="user.id"
|
||||
:avatar="user.avatar"
|
||||
:name="user.name"
|
||||
:size="30"
|
||||
/>
|
||||
<span v-if="contributors.length===0">No users with this role.</span>
|
||||
<span v-if="contributors.length === 0">No users with this role.</span>
|
||||
</div>
|
||||
<div v-if="role.name === 'stream:owner'">
|
||||
<user-avatar
|
||||
v-for="user in owners"
|
||||
:key="user.id"
|
||||
:id="user.id"
|
||||
:key="user.id"
|
||||
:avatar="user.avatar"
|
||||
:name="user.name"
|
||||
:size="30"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -79,9 +81,9 @@
|
||||
</template>
|
||||
|
||||
<v-toolbar
|
||||
v-if="stream.role === 'stream:owner'"
|
||||
flat
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`"
|
||||
v-if="stream.role === 'stream:owner'"
|
||||
>
|
||||
<v-toolbar-title>
|
||||
<v-icon small class="mr-2">mdi-account-plus</v-icon>
|
||||
@@ -152,9 +154,9 @@
|
||||
:class="`mt-5 ${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`"
|
||||
>
|
||||
<v-toolbar
|
||||
v-if="stream.role === 'stream:owner'"
|
||||
flat
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`"
|
||||
v-if="stream.role === 'stream:owner'"
|
||||
>
|
||||
<v-toolbar-title>
|
||||
<v-icon small class="mr-2">mdi-account-group</v-icon>
|
||||
@@ -179,8 +181,8 @@
|
||||
item-value="name"
|
||||
:items="roles"
|
||||
class="py-0 my-0"
|
||||
@change="setUserPermissions(user)"
|
||||
:disabled="stream.role !== 'stream:owner'"
|
||||
@change="setUserPermissions(user)"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
{{ item.name }}
|
||||
@@ -201,8 +203,8 @@
|
||||
icon
|
||||
small
|
||||
color="error"
|
||||
@click="removeUser(user)"
|
||||
:disabled="stream.role !== 'stream:owner'"
|
||||
@click="removeUser(user)"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
@@ -294,10 +296,10 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRoleCount( role ) {
|
||||
if(role === 'stream:owner') return this.owners.length || '0'
|
||||
if(role === 'stream:contributor') return this.contributors.length || '0'
|
||||
if(role === 'stream:reviewer') return this.reviewers.length || '0'
|
||||
getRoleCount(role) {
|
||||
if (role === 'stream:owner') return this.owners.length || '0'
|
||||
if (role === 'stream:contributor') return this.contributors.length || '0'
|
||||
if (role === 'stream:reviewer') return this.reviewers.length || '0'
|
||||
},
|
||||
async removeUser(user) {
|
||||
this.loading = true
|
||||
@@ -368,4 +370,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
+228
-66
@@ -2,13 +2,13 @@
|
||||
<v-container fluid pa-0 ma-0>
|
||||
<!-- Stream Page Navigation Drawer -->
|
||||
<v-navigation-drawer
|
||||
v-if="!error"
|
||||
v-model="streamNav"
|
||||
app
|
||||
fixed
|
||||
clipped
|
||||
:permanent="streamNav && !$vuetify.breakpoint.smAndDown"
|
||||
v-model="streamNav"
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
|
||||
v-if="!error"
|
||||
>
|
||||
<!-- Toolbar holds link to stream home page -->
|
||||
<v-app-bar
|
||||
@@ -19,9 +19,9 @@
|
||||
>
|
||||
<v-toolbar-title>
|
||||
<router-link
|
||||
v-tooltip="stream.name"
|
||||
:to="`/streams/${stream.id}`"
|
||||
class="text-decoration-none space-grotesk"
|
||||
v-tooltip="stream.name"
|
||||
>
|
||||
<v-icon class="mr-2 primary--text" style="font-size: 20px">mdi-folder</v-icon>
|
||||
<b>{{ stream.name }}</b>
|
||||
@@ -32,18 +32,18 @@
|
||||
<!-- <v-skeleton-loader v-else type="list-item-two-line"></v-skeleton-loader> -->
|
||||
|
||||
<!-- Top padding hack -->
|
||||
<div style="display: block; height: 65px" v-if="$vuetify.breakpoint.smAndDown"></div>
|
||||
<div class="px-4 mt-2" v-if="!loggedIn">
|
||||
<div v-if="$vuetify.breakpoint.smAndDown" style="display: block; height: 65px"></div>
|
||||
<div v-if="!loggedIn" class="px-4 mt-2">
|
||||
<v-btn large block color="primary" to="/authn/login">Log In</v-btn>
|
||||
</div>
|
||||
<!-- Various Stream Details -->
|
||||
<v-card elevation="0" v-if="stream" class="pa-1 mb-0" color="transparent">
|
||||
<v-card v-if="stream" elevation="0" class="pa-1 mb-0" color="transparent">
|
||||
<v-card-text class="caption">
|
||||
<span v-html="parsedDescription"></span>
|
||||
<router-link
|
||||
v-if="stream.role === 'stream:owner'"
|
||||
:to="`/streams/${$route.params.streamId}/settings`"
|
||||
class="text-decoration-none"
|
||||
v-if="stream.role === 'stream:owner'"
|
||||
>
|
||||
Edit
|
||||
</router-link>
|
||||
@@ -78,19 +78,19 @@
|
||||
:name="collab.name"
|
||||
></user-avatar>
|
||||
<v-btn
|
||||
v-if="stream.collaborators.length > 5"
|
||||
v-tooltip="`${stream.collaborators.length - 4} more collaborators`"
|
||||
icon
|
||||
:to="`/streams/${stream.id}/collaborators`"
|
||||
v-tooltip="`${stream.collaborators.length - 4} more collaborators`"
|
||||
v-if="stream.collaborators.length > 5"
|
||||
>
|
||||
<span class="text-subtitle-1">+{{ stream.collaborators.length - 4 }}</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="stream.collaborators.length <= 5"
|
||||
v-tooltip="'Manage collaborators'"
|
||||
icon
|
||||
:to="`/streams/${stream.id}/collaborators`"
|
||||
class="ml-2"
|
||||
v-tooltip="'Manage collaborators'"
|
||||
v-if="stream.collaborators.length <= 5"
|
||||
>
|
||||
<v-avatar>
|
||||
<v-icon>mdi-account-plus</v-icon>
|
||||
@@ -106,7 +106,7 @@
|
||||
</v-card>
|
||||
|
||||
<!-- Stream menu options -->
|
||||
<v-list style="padding-left: 10px" rounded dense class="mt-0 pt-0" v-if="stream">
|
||||
<v-list v-if="stream" style="padding-left: 10px" rounded dense class="mt-0 pt-0" expand>
|
||||
<v-list-item link :to="`/streams/${stream.id}`" class="no-overlay">
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-home</v-icon>
|
||||
@@ -117,9 +117,8 @@
|
||||
</v-list-item>
|
||||
|
||||
<!-- Branch menu group -->
|
||||
<!-- TODO: group by "/", eg. dim/a, dim/b, dim/c should be under a sub-group called "dim". -->
|
||||
<v-list-group v-model="branchMenuOpen" class="my-2">
|
||||
<template v-slot:activator>
|
||||
<template #activator>
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-source-branch</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -127,9 +126,9 @@
|
||||
</template>
|
||||
<v-divider class="mb-1"></v-divider>
|
||||
<v-list-item
|
||||
link
|
||||
v-tooltip.bottom="'Create a new branch to help categorise your commits.'"
|
||||
v-if="stream.role !== 'stream:reviewer'"
|
||||
v-tooltip.bottom="'Create a new branch to help categorise your commits.'"
|
||||
link
|
||||
@click="showNewBranchDialog()"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
@@ -143,28 +142,77 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="!$apollo.queries.branchQuery.loading"
|
||||
v-for="(branch, i) in sortedBranches"
|
||||
:key="i"
|
||||
link
|
||||
:to="`/streams/${stream.id}/branches/${branch.name}`"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon small style="padding-top: 10px" v-if="branch.name !== 'main'">
|
||||
mdi-source-branch
|
||||
</v-icon>
|
||||
<v-icon small style="padding-top: 10px" class="primary--text" v-else>mdi-star</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ branch.name }} ({{ branch.commits.totalCount }})
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ branch.description ? branch.description : 'no description' }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<!-- TODO -->
|
||||
<div v-if="!$apollo.queries.branchQuery.loading">
|
||||
<template v-for="(item, i) in groupedBranches">
|
||||
<v-list-item
|
||||
v-if="item.type === 'item'"
|
||||
:key="i"
|
||||
:to="`/streams/${stream.id}/branches/${item.name}`"
|
||||
exact
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="item.name !== 'main'" small style="padding-top: 10px">
|
||||
mdi-source-branch
|
||||
</v-icon>
|
||||
<v-icon v-else small style="padding-top: 10px" class="primary--text">
|
||||
mdi-star
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ item.displayName }} ({{ item.commits.totalCount }})
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ item.description ? item.description : 'no description' }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-group
|
||||
v-else
|
||||
:key="i"
|
||||
sub-group
|
||||
:value="item.expand"
|
||||
prepend-icon=""
|
||||
:group="item.name"
|
||||
>
|
||||
<template #activator>
|
||||
<v-list-item style="overflow: visible">
|
||||
<v-list-item-icon style="position: relative; left: -26px">
|
||||
<v-icon style="padding-top: 10px">
|
||||
{{ item.expand ? 'mdi-chevron-down' : 'mdi-chevron-down' }}
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content style="position: relative; left: -8px">
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ item.children.length }} branches
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-list-item
|
||||
v-for="(kid, j) in item.children"
|
||||
:key="j"
|
||||
:to="`/streams/${stream.id}/branches/${kid.name}`"
|
||||
exact
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon small style="padding-top: 10px">mdi-source-branch</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ kid.displayName }} ({{ kid.commits.totalCount }})
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
{{ kid.description ? kid.description : 'no description' }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-group>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<v-skeleton-loader v-else type="list-item-two-line"></v-skeleton-loader>
|
||||
<v-divider class="mb-2"></v-divider>
|
||||
</v-list-group>
|
||||
@@ -180,6 +228,15 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link :to="`/streams/${stream.id}/uploads`">
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-arrow-up</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Import IFC</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link :to="`/streams/${stream.id}/webhooks`">
|
||||
<v-list-item-icon>
|
||||
<v-icon small>mdi-webhook</v-icon>
|
||||
@@ -211,25 +268,25 @@
|
||||
|
||||
<!-- Stream Page App Bar -->
|
||||
<v-app-bar
|
||||
v-if="!error"
|
||||
app
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`"
|
||||
flat
|
||||
clipped-left
|
||||
v-if="!error"
|
||||
>
|
||||
<v-app-bar-nav-icon @click="streamNav = !streamNav" v-if="true || !streamNav">
|
||||
<v-app-bar-nav-icon v-if="true || !streamNav" @click="streamNav = !streamNav">
|
||||
<!-- <v-icon v-if="streamNav">mdi-chevron-left</v-icon> -->
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title class="pl-0">
|
||||
<router-link
|
||||
v-if="stream"
|
||||
v-show="true || !streamNav && !$vuetify.breakpoint.smAndDown"
|
||||
v-show="true || (!streamNav && !$vuetify.breakpoint.smAndDown)"
|
||||
class="text-decoration-none space-grotesk"
|
||||
:to="`/streams/${stream.id}`"
|
||||
>
|
||||
<b>{{ stream.name }}</b>
|
||||
</router-link>
|
||||
<span class="mx-2" v-show="true || !streamNav && !$vuetify.breakpoint.smAndDown">/</span>
|
||||
<span v-show="true || (!streamNav && !$vuetify.breakpoint.smAndDown)" class="mx-2">/</span>
|
||||
<portal-target name="streamTitleBar" slim style="display: inline-block">
|
||||
<!-- child routes can teleport things here -->
|
||||
</portal-target>
|
||||
@@ -239,17 +296,17 @@
|
||||
<!-- child routes can teleport buttons here -->
|
||||
</portal-target>
|
||||
<v-toolbar-items style="margin-right: -20px">
|
||||
<v-btn large color="primary" to="/authn/login" v-if="!loggedIn && stream && !streamNav">
|
||||
<v-btn v-if="!loggedIn && stream && !streamNav" large color="primary" to="/authn/login">
|
||||
Log In
|
||||
</v-btn>
|
||||
<v-btn
|
||||
elevation="0"
|
||||
v-if="loggedIn && stream"
|
||||
@click="openShareStreamDialog()"
|
||||
v-tooltip="'Share this stream'"
|
||||
elevation="0"
|
||||
@click="openShareStreamDialog()"
|
||||
>
|
||||
<v-icon small class="mr-2 grey--text" v-if="!stream.isPublic">mdi-lock</v-icon>
|
||||
<v-icon small class="mr-2 grey--text" v-else>mdi-lock-open</v-icon>
|
||||
<v-icon v-if="!stream.isPublic" small class="mr-2 grey--text">mdi-lock</v-icon>
|
||||
<v-icon v-else small class="mr-2 grey--text">mdi-lock-open</v-icon>
|
||||
<v-icon small class="mr-2">mdi-share-variant</v-icon>
|
||||
<span class="hidden-md-and-down">Share</span>
|
||||
</v-btn>
|
||||
@@ -258,18 +315,18 @@
|
||||
|
||||
<!-- Stream Child Routes -->
|
||||
<v-container
|
||||
v-if="!error"
|
||||
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px;' : ''}`"
|
||||
:class="`${$vuetify.breakpoint.xsOnly ? 'pl-0' : ''}`"
|
||||
fluid
|
||||
pt-0
|
||||
pr-0
|
||||
v-if="!error"
|
||||
>
|
||||
<transition name="fade">
|
||||
<router-view v-if="stream" @refetch-branches="refetchBranches"></router-view>
|
||||
</transition>
|
||||
</v-container>
|
||||
<v-container :style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" v-else>
|
||||
<v-container v-else :style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`">
|
||||
<error-placeholder :error-type="error.toLowerCase().includes('not found') ? '404' : 'access'">
|
||||
<h2>{{ error }}</h2>
|
||||
</error-placeholder>
|
||||
@@ -290,8 +347,8 @@
|
||||
</v-toolbar>
|
||||
<v-card-text class="mt-0 mb-0 px-2">
|
||||
<v-text-field
|
||||
dark
|
||||
ref="streamUrl"
|
||||
dark
|
||||
filled
|
||||
rounded
|
||||
hint="Stream url copied to clipboard. Use it in a connector, or just share it with colleagues!"
|
||||
@@ -302,8 +359,8 @@
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-if="$route.params.branchName"
|
||||
dark
|
||||
ref="branchUrl"
|
||||
dark
|
||||
filled
|
||||
rounded
|
||||
hint="Branch url copied to clipboard. Most connectors can receive the latest commit from a branch by using this url."
|
||||
@@ -314,8 +371,8 @@
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-if="$route.params.commitId"
|
||||
dark
|
||||
ref="commitUrl"
|
||||
dark
|
||||
filled
|
||||
rounded
|
||||
hint="Commit url copied to clipboard. Most connectors can receive a specific commit by using this url."
|
||||
@@ -327,10 +384,10 @@
|
||||
</v-card-text>
|
||||
</v-sheet>
|
||||
<v-sheet
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
|
||||
v-if="stream"
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
|
||||
>
|
||||
<v-toolbar class="transparent" rounded v-if="stream.role === 'stream:owner'" flat>
|
||||
<v-toolbar v-if="stream.role === 'stream:owner'" class="transparent" rounded flat>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>{{ stream.isPublic ? 'mdi-lock-open' : 'mdi-lock' }}</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
@@ -339,9 +396,9 @@
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-switch
|
||||
v-model="stream.isPublic"
|
||||
inset
|
||||
class="mt-4"
|
||||
v-model="stream.isPublic"
|
||||
:loading="swapPermsLoading"
|
||||
@click="changeVisibility"
|
||||
></v-switch>
|
||||
@@ -356,7 +413,6 @@
|
||||
</v-sheet>
|
||||
<v-sheet v-if="stream">
|
||||
<v-toolbar
|
||||
flat
|
||||
v-tooltip="
|
||||
`${
|
||||
stream.role !== 'stream:owner'
|
||||
@@ -366,6 +422,7 @@
|
||||
: ''
|
||||
}`
|
||||
"
|
||||
flat
|
||||
>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-account-group</v-icon>
|
||||
@@ -401,8 +458,6 @@
|
||||
:xxxclass="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
|
||||
>
|
||||
<v-toolbar
|
||||
flat
|
||||
class="transparent"
|
||||
v-if="!stream.isPublic"
|
||||
v-tooltip="
|
||||
`${
|
||||
@@ -413,6 +468,8 @@
|
||||
: ''
|
||||
}`
|
||||
"
|
||||
flat
|
||||
class="transparent"
|
||||
>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-email</v-icon>
|
||||
@@ -423,8 +480,8 @@
|
||||
color="primary"
|
||||
text
|
||||
rounded
|
||||
@click="showStreamInviteDialog()"
|
||||
:disabled="stream.role !== 'stream:owner'"
|
||||
@click="showStreamInviteDialog()"
|
||||
>
|
||||
Send Invite
|
||||
</v-btn>
|
||||
@@ -438,6 +495,27 @@
|
||||
:stream-id="$route.params.streamId"
|
||||
:stream-name="stream.name"
|
||||
/>
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
rounded="pill"
|
||||
:timeout="10000"
|
||||
style="z-index: 10000"
|
||||
:color="`${$vuetify.theme.dark ? 'primary' : 'primary'}`"
|
||||
>
|
||||
<template v-if="snackbarInfo.type === 'commit'">
|
||||
<span>New commit created!</span>
|
||||
</template>
|
||||
<template v-if="snackbarInfo.type === 'branch'">
|
||||
<span>Branch "{{ snackbarInfo.name }}" created!</span>
|
||||
</template>
|
||||
|
||||
<template #action="{ attrs }">
|
||||
<v-btn color="white" text v-bind="attrs" @click="goToItemAndCloseSnackbar()">View</v-btn>
|
||||
<v-btn color="pink" icon v-bind="attrs" @click="snackbar = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
@@ -456,8 +534,8 @@ export default {
|
||||
return {
|
||||
streamNav: true,
|
||||
error: '',
|
||||
commitSnackbar: false,
|
||||
commitSnackbarInfo: {},
|
||||
snackbar: false,
|
||||
snackbarInfo: {},
|
||||
editStreamDialog: false,
|
||||
shareStream: false,
|
||||
branchMenuOpen: false,
|
||||
@@ -509,6 +587,10 @@ export default {
|
||||
items {
|
||||
name
|
||||
description
|
||||
author {
|
||||
id
|
||||
name
|
||||
}
|
||||
commits {
|
||||
totalCount
|
||||
}
|
||||
@@ -521,9 +603,33 @@ export default {
|
||||
return {
|
||||
id: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
update: (data) => {
|
||||
// console.log(data.branchQuery.branches.items)
|
||||
return data.branchQuery
|
||||
}
|
||||
},
|
||||
$subscribe: {
|
||||
branchCreated: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
branchCreated(streamId: $streamId)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId
|
||||
}
|
||||
},
|
||||
result(args) {
|
||||
if (!args.data.branchCreated) return
|
||||
this.snackbar = true
|
||||
this.snackbarInfo = { ...args.data.branchCreated, type: 'branch' }
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
}
|
||||
},
|
||||
commitCreated: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
@@ -537,8 +643,9 @@ export default {
|
||||
},
|
||||
result(commitInfo) {
|
||||
if (!commitInfo.data.commitCreated) return
|
||||
this.commitSnackbar = true
|
||||
this.commitSnackbarInfo = commitInfo.data.commitCreated
|
||||
console.log(commitInfo)
|
||||
this.snackbar = true
|
||||
this.snackbarInfo = { ...commitInfo.data.commitCreated, type: 'commit' }
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
@@ -547,6 +654,43 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
groupedBranches() {
|
||||
if (!this.branchQuery) return
|
||||
let branches = this.branchQuery.branches.items
|
||||
let items = []
|
||||
for (let b of branches) {
|
||||
if (b.name === 'globals') continue
|
||||
let parts = b.name.split('/')
|
||||
if (parts.length === 1) {
|
||||
items.push({ ...b, displayName: b.name, type: 'item', children: [] })
|
||||
} else {
|
||||
let existing = items.find((i) => i.name === parts[0] && i.type === 'group')
|
||||
if (!existing) {
|
||||
existing = { name: parts[0], type: 'group', children: [], expand: false }
|
||||
items.push(existing)
|
||||
}
|
||||
existing.children.push({
|
||||
...b,
|
||||
displayName: parts.slice(1).join('/'),
|
||||
type: 'item'
|
||||
})
|
||||
if (this.$route.path.includes(b.name)) existing.expand = true
|
||||
}
|
||||
}
|
||||
let sorted = items.sort((a, b) => {
|
||||
const nameA = a.name.toLowerCase()
|
||||
const nameB = b.name.toLowerCase()
|
||||
if (nameA < nameB) return -1
|
||||
if (nameA > nameB) return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
return [
|
||||
...sorted.filter((it) => it.name === 'main'),
|
||||
...sorted.filter((it) => it.name !== 'main')
|
||||
]
|
||||
// return items
|
||||
},
|
||||
streamUrl() {
|
||||
return `${window.location.origin}/streams/${this.$route.params.streamId}`
|
||||
},
|
||||
@@ -580,13 +724,20 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
$route(to) {
|
||||
// Ensures branch menu is open when navigating to a branch url
|
||||
if (this.$route.name.toLowerCase().includes('branch') && !this.branchMenuOpen)
|
||||
if (to.name.toLowerCase().includes('branch') && !this.branchMenuOpen)
|
||||
this.branchMenuOpen = true
|
||||
|
||||
// closes any share dialog
|
||||
this.shareStream = false
|
||||
this.snackbar = false
|
||||
}
|
||||
// branchMenuOpen(val) {
|
||||
// if (this.$route.name.toLowerCase().includes('branch') && !val)
|
||||
// this.$nextTick(() => {
|
||||
// this.branchMenuOpen = true
|
||||
// })
|
||||
// }
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(
|
||||
@@ -606,6 +757,17 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToItemAndCloseSnackbar() {
|
||||
if (this.snackbarInfo.type === 'commit') {
|
||||
this.$router.push(`/streams/${this.$route.params.streamId}/commits/${this.snackbarInfo.id}`)
|
||||
} else if (this.snackbarInfo.type === 'branch') {
|
||||
this.$router.push(
|
||||
`/streams/${this.$route.params.streamId}/branches/${this.snackbarInfo.name}`
|
||||
)
|
||||
this.refetchBranches()
|
||||
}
|
||||
this.snackbar = false
|
||||
},
|
||||
copyToClipboard(e) {
|
||||
e.target.select()
|
||||
document.execCommand('copy')
|
||||
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-container style="max-width: 768px">
|
||||
<portal to="streamTitleBar">
|
||||
<div>
|
||||
<v-icon small class="mr-2 hidden-xs-only">mdi-arrow-up</v-icon>
|
||||
<span class="space-grotesk">Import IFC</span>
|
||||
</div>
|
||||
</portal>
|
||||
|
||||
<v-card elevation="1" rounded="lg" :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'white' : ''} mb-2`">
|
||||
<v-toolbar-title>
|
||||
<v-icon class="mr-2" small>mdi-arrow-up</v-icon>
|
||||
<span class="d-inline-block">Import IFC Files - Alpha</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text>
|
||||
Speckle can now process IFC files and store them as a commit (snapshot). You can then
|
||||
access it from the Speckle API, and receive it in other applications.
|
||||
<!-- </v-card-text>
|
||||
<v-card-text> -->
|
||||
Thanks to the Open Source
|
||||
<a href="https://ifcjs.github.io/info/docs/Guide/web-ifc/Introduction" target="_blank">
|
||||
IFC.js Project
|
||||
</a>
|
||||
for making this possible.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-alert v-if="stream && (stream.role === 'stream:reviewer' || !stream.role)" type="warning">
|
||||
Your permission level ({{ stream.role ? stream.role : 'none' }}) is not high enough to
|
||||
access this feature.
|
||||
</v-alert>
|
||||
|
||||
<div v-if="stream && !(stream.role === 'stream:reviewer' || !stream.role)">
|
||||
<v-card
|
||||
elevation="0"
|
||||
color="transparent"
|
||||
class=""
|
||||
style="height: 220px; transition: all 0.2s ease"
|
||||
:class="`mt-4 mb-4 d-flex justify-center
|
||||
${dragover && !$vuetify.theme.dark ? 'grey lighten-4' : ''}
|
||||
${dragover && $vuetify.theme.dark ? 'grey darken-4' : ''}
|
||||
`"
|
||||
@drop.prevent="onFileDrop($event)"
|
||||
@dragover.prevent="dragover = true"
|
||||
@dragenter.prevent="dragover = true"
|
||||
@dragleave.prevent="dragover = false"
|
||||
>
|
||||
<div v-if="!dragError" class="align-self-center text-center">
|
||||
<input
|
||||
id="myid"
|
||||
type="file"
|
||||
accept=".ifc,.IFC"
|
||||
style="display: none"
|
||||
multiple
|
||||
@change="onFileSelect($event)"
|
||||
/>
|
||||
<v-icon
|
||||
x-large
|
||||
color="primary"
|
||||
:class="`hover-tada ${dragover ? 'tada' : ''}`"
|
||||
style="cursor: pointer"
|
||||
onclick="document.getElementById('myid').click()"
|
||||
>
|
||||
mdi-cloud-upload
|
||||
</v-icon>
|
||||
<br />
|
||||
<span class="primary--text">Drag and drop your IFC file here!</span>
|
||||
<br />
|
||||
<span class="caption">Maximum 5 files at a time. Size is restricted to 50mb each.</span>
|
||||
</div>
|
||||
<v-alert
|
||||
v-if="dragError"
|
||||
dismissible
|
||||
class="align-self-center text-center"
|
||||
type="error"
|
||||
@click="dragError = null"
|
||||
>
|
||||
{{ dragError }}
|
||||
</v-alert>
|
||||
</v-card>
|
||||
|
||||
<!-- {{ uploads }} -->
|
||||
<template v-for="file in files">
|
||||
<file-upload-item
|
||||
:key="file.fileName"
|
||||
:file="file"
|
||||
:branches="stream.branches.items"
|
||||
@done="uploadCompleted"
|
||||
></file-upload-item>
|
||||
</template>
|
||||
</div>
|
||||
<v-card elevation="1" rounded="lg" :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'white' : ''} mb-2`">
|
||||
<v-toolbar-title>
|
||||
<!-- <v-icon class="mr-2" small>mdi-arrow-up</v-icon> -->
|
||||
<span class="d-inline-block">Previous Uploads</span>
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text>
|
||||
Here are the previously uploaded files in this stream. Please note, currently processing
|
||||
time is restricted to 5 minutes - if a file takes longer to process, it will be ignored.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<template v-for="file in streamUploads" v-if="!$apollo.loading">
|
||||
<file-processing-item :key="file.id" :file-id="file.id" />
|
||||
</template>
|
||||
<v-card v-if="!$apollo.loading && streamUploads.length === 0" class="my-4 elevation-1">
|
||||
<v-toolbar dense flat color="transparent">
|
||||
<v-toolbar-title>No uploads yet.</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-toolbar>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'Webhooks',
|
||||
components: {
|
||||
FileUploadItem: () => import('@/components/FileUploadItem'),
|
||||
FileProcessingItem: () => import('@/components/FileProcessingItem')
|
||||
},
|
||||
apollo: {
|
||||
stream: {
|
||||
query: gql`
|
||||
query stream($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
role
|
||||
branches {
|
||||
totalCount
|
||||
items {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.$route.params.streamId }
|
||||
}
|
||||
},
|
||||
streamUploads: {
|
||||
query: gql`
|
||||
query streamUploads($streamId: String!) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
role
|
||||
fileUploads {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
update: (data) => data.stream.fileUploads,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragover: false,
|
||||
loading: false,
|
||||
stream: null,
|
||||
files: [],
|
||||
showUploadDialog: false,
|
||||
error: null,
|
||||
dragError: null
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
onFileSelect(e) {
|
||||
this.parseFiles(e.target.files)
|
||||
},
|
||||
onFileDrop(e) {
|
||||
this.parseFiles(e.dataTransfer.files)
|
||||
},
|
||||
parseFiles(files) {
|
||||
this.dragover = false
|
||||
this.dragError = null
|
||||
for (const file of files) {
|
||||
console.log(file.name.split('.')[1])
|
||||
let extension = file.name.split('.')[1]
|
||||
if (!extension || extension !== 'ifc') {
|
||||
this.dragError = 'Only IFC file extensions are supported.'
|
||||
return
|
||||
}
|
||||
|
||||
if (file.size > 50626997) {
|
||||
this.dragError = 'Your files are too powerful (for now). Maximum upload size is 50mb!'
|
||||
return
|
||||
}
|
||||
|
||||
if (this.files.findIndex((f) => f.name === file.name) !== -1) {
|
||||
this.dragError = 'This file is already primed for upload.'
|
||||
return
|
||||
}
|
||||
}
|
||||
if (files.length > 5) {
|
||||
this.dragError = 'Maximum five files at a time allowed.'
|
||||
return
|
||||
}
|
||||
this.dragError = null
|
||||
|
||||
for (const file of files) {
|
||||
this.files.push(file)
|
||||
}
|
||||
},
|
||||
uploadCompleted(file) {
|
||||
const index = this.files.findIndex((f) => f.name === file)
|
||||
this.files.splice(index, 1)
|
||||
this.$apollo.queries.streamUploads.refetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<no-data-placeholder
|
||||
:show-message="false"
|
||||
v-if="!$apollo.loading && webhooks.length === 0 && stream && stream.role === 'stream:owner'"
|
||||
:show-message="false"
|
||||
>
|
||||
<h2>This stream has no webhooks.</h2>
|
||||
<p class="caption">
|
||||
Webhooks allow you to subscribe to a stream's events and get notified of them in real time.
|
||||
You can then use this to trigger ci apps, automation workflows, and more.
|
||||
</p>
|
||||
<template v-slot:actions>
|
||||
<template #actions>
|
||||
<v-list rounded class="transparent">
|
||||
<v-list-item link class="primary mb-4" dark @click="newWebhookDialog = true">
|
||||
<v-list-item-icon>
|
||||
@@ -39,24 +39,22 @@
|
||||
</v-list>
|
||||
</template>
|
||||
</no-data-placeholder>
|
||||
|
||||
<error-placeholder error-type="access" v-if="error">
|
||||
|
||||
<error-placeholder v-if="error" error-type="access">
|
||||
<h2>Only stream owners can access webhooks.</h2>
|
||||
<p class="caption">If you need to use webhooks, ask the stream's owner to grant you ownership.</p>
|
||||
<p class="caption">
|
||||
If you need to use webhooks, ask the stream's owner to grant you ownership.
|
||||
</p>
|
||||
</error-placeholder>
|
||||
|
||||
<v-container style="max-width: 768px" v-if="!$apollo.loading && webhooks.length !== 0">
|
||||
<v-container v-if="!$apollo.loading && webhooks.length !== 0" style="max-width: 768px">
|
||||
<portal to="streamTitleBar">
|
||||
<div>
|
||||
<v-icon small class="mr-2 hidden-xs-only">mdi-webhook</v-icon>
|
||||
<span class="space-grotesk">Webhooks</span>
|
||||
</div>
|
||||
</portal>
|
||||
<v-card
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`"
|
||||
>
|
||||
<v-card elevation="0" rounded="lg" :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
|
||||
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`">
|
||||
<v-toolbar-title>
|
||||
<v-icon class="mr-2" small>mdi-webhook</v-icon>
|
||||
@@ -85,7 +83,7 @@
|
||||
<span class="d-inline-block">Existing Webhooks</span>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="newWebhookDialog = true" small class="primary" dark>New Webhook</v-btn>
|
||||
<v-btn small class="primary" dark @click="newWebhookDialog = true">New Webhook</v-btn>
|
||||
</v-toolbar>
|
||||
<v-list subheader class="transparent pa-0 ma-0">
|
||||
<v-list-item v-for="wh in webhooks" :key="wh.id" link style="cursor: default">
|
||||
@@ -107,12 +105,12 @@
|
||||
</v-list-item-content>
|
||||
<v-list-item-action v-if="wh.history.items.length != 0">
|
||||
<v-btn
|
||||
v-tooltip="'View status reports'"
|
||||
icon
|
||||
@click="
|
||||
selectedWebhook = wh
|
||||
statusReportsDialog = true
|
||||
"
|
||||
icon
|
||||
v-tooltip="'View status reports'"
|
||||
>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</v-btn>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
module.exports = {
|
||||
configureWebpack: {
|
||||
devtool: 'source-map'
|
||||
},
|
||||
productionSourceMap: false,
|
||||
pages: {
|
||||
app: {
|
||||
@@ -16,6 +19,7 @@ module.exports = {
|
||||
},
|
||||
devServer: {
|
||||
host: 'localhost',
|
||||
proxy: 'http://localhost:3000',
|
||||
historyApiFallback: {
|
||||
rewrites: [
|
||||
{ from: /^\/$/, to: '/app.html' },
|
||||
|
||||
@@ -28,6 +28,23 @@ POSTGRES_PASSWORD="speckle"
|
||||
# this overrides the default database name in postgres
|
||||
POSTGRES_DB="speckle"
|
||||
|
||||
############################################################
|
||||
# Object storage (S3)
|
||||
############################################################
|
||||
# Uncomment to disable file uploads
|
||||
# DISABLE_FILE_UPLOADS="true"
|
||||
|
||||
# S3 Endpoint and credentials
|
||||
S3_ENDPOINT="http://127.0.0.1:9000"
|
||||
S3_ACCESS_KEY="minioadmin"
|
||||
S3_SECRET_KEY="minioadmin"
|
||||
|
||||
# Bucket where to store the files
|
||||
S3_BUCKET="speckle-server"
|
||||
|
||||
# Try to create bucket at startup if it doesn't exist
|
||||
S3_CREATE_BUCKET="true"
|
||||
|
||||
############################################################
|
||||
# Emails
|
||||
############################################################
|
||||
|
||||
@@ -17,11 +17,9 @@ module.exports = async () => {
|
||||
await registerOrUpdateApp( { ...SpeckleApiExplorer } )
|
||||
await registerOrUpdateApp( { ...SpeckleDesktopApp } )
|
||||
await registerOrUpdateApp( { ...SpeckleExcel } )
|
||||
|
||||
}
|
||||
|
||||
async function registerOrUpdateApp( app ) {
|
||||
|
||||
if ( app.scopes && app.scopes === 'all' ) {
|
||||
// let scopes = await Scopes( ).select( '*' )
|
||||
// console.log( allScopes.length )
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use strict'
|
||||
const passport = require( 'passport' )
|
||||
const URL = require( 'url' ).URL
|
||||
const appRoot = require( 'app-root-path' )
|
||||
const debug = require( 'debug' )
|
||||
const { createUser, updateUser, findOrCreateUser, validatePasssword, getUserByEmail } = require( `${appRoot}/modules/core/services/users` )
|
||||
const { createUser, updateUser, validatePasssword, getUserByEmail } = require( `${appRoot}/modules/core/services/users` )
|
||||
const { getServerInfo } = require( `${appRoot}/modules/core/services/generic` )
|
||||
const { validateInvite, useInvite } = require( `${appRoot}/modules/serverinvites/services` )
|
||||
|
||||
@@ -40,7 +39,6 @@ module.exports = async ( app, session, sessionAppId, finalizeAuth ) => {
|
||||
app.post( '/auth/local/register', session, sessionAppId, async ( req, res, next ) => {
|
||||
const serverInfo = await getServerInfo()
|
||||
try {
|
||||
|
||||
if ( !req.body.password )
|
||||
throw new Error( 'Password missing' )
|
||||
|
||||
@@ -62,7 +60,6 @@ module.exports = async ( app, session, sessionAppId, finalizeAuth ) => {
|
||||
req.user = { id: userId, email: user.email }
|
||||
|
||||
return next( )
|
||||
|
||||
} catch ( err ) {
|
||||
debug( 'speckle:errors' )( err )
|
||||
return res.status( 400 ).send( { err: err.message } )
|
||||
|
||||
@@ -41,7 +41,7 @@ module.exports = {
|
||||
Branch: {
|
||||
|
||||
async author( parent, args, context, info ) {
|
||||
if ( parent.userId )
|
||||
if ( parent.authorId && context.auth )
|
||||
return await getUserById( { userId: parent.authorId } )
|
||||
else return null
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
const appRoot = require( 'app-root-path' )
|
||||
const { UserInputError } = require( 'apollo-server-express' )
|
||||
const { getUser, getUserRole, updateUser, deleteUser, searchUsers, getUserById } = require( '../../services/users' )
|
||||
const { getUser, getUsers, countUsers, getUserRole, updateUser, deleteUser, searchUsers, getUserById, makeUserAdmin, unmakeUserAdmin } = require( '../../services/users' )
|
||||
const { saveActivity } = require( `${appRoot}/modules/activitystream/services` )
|
||||
const { validateServerRole, validateScopes } = require( `${appRoot}/modules/shared` )
|
||||
const zxcvbn = require( 'zxcvbn' )
|
||||
@@ -28,6 +28,14 @@ module.exports = {
|
||||
return await getUser( args.id || context.userId )
|
||||
},
|
||||
|
||||
async users( parent, args, context, info ){
|
||||
await validateServerRole( context, 'server:admin' )
|
||||
await validateScopes( context.scopes, 'users:read' )
|
||||
let users = await getUsers ( args.limit, args.offset, args.query )
|
||||
let totalCount = await countUsers( args.query )
|
||||
return { totalCount, items: users }
|
||||
},
|
||||
|
||||
async userSearch( parent, args, context, info ) {
|
||||
await validateServerRole( context, 'server:user' )
|
||||
await validateScopes( context.scopes, 'profile:read' )
|
||||
@@ -99,6 +107,12 @@ module.exports = {
|
||||
return true
|
||||
},
|
||||
|
||||
async userRoleChange( parent, args, context, info ) {
|
||||
let roleChanger = args.userRoleInput.role === 'server:admin' ? makeUserAdmin: unmakeUserAdmin
|
||||
await roleChanger( { userId: args.userRoleInput.id } )
|
||||
return true
|
||||
},
|
||||
|
||||
async userDelete( parent, args, context, info ) {
|
||||
let user = await getUser( context.userId )
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ extend type Query {
|
||||
Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
|
||||
"""
|
||||
user(id: String): User
|
||||
|
||||
"""
|
||||
Get users from the server in a paginated view. The query search for matches in name, company and email.
|
||||
"""
|
||||
users(limit: Int!=25, offset: Int!=0, query: String=null) : UserCollection
|
||||
userSearch(
|
||||
query: String!
|
||||
limit: Int! = 25
|
||||
@@ -27,6 +32,12 @@ type User {
|
||||
role: String
|
||||
}
|
||||
|
||||
|
||||
type UserCollection {
|
||||
totalCount: Int!
|
||||
items: [ User ]
|
||||
}
|
||||
|
||||
type UserSearchResultCollection {
|
||||
cursor: String
|
||||
items: [UserSearchResult]
|
||||
@@ -53,6 +64,14 @@ extend type Mutation {
|
||||
userDelete(userConfirmation: UserDeleteInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasScope(scope: "profile:delete")
|
||||
|
||||
userRoleChange(userRoleInput: UserRoleInput!): Boolean!
|
||||
@hasRole(role: "server:admin")
|
||||
}
|
||||
|
||||
input UserRoleInput {
|
||||
id: String!
|
||||
role: String!
|
||||
}
|
||||
|
||||
input UserUpdateInput {
|
||||
|
||||
@@ -14,8 +14,8 @@ const { getObjectsStream } = require( '../services/objects' )
|
||||
const { pipeline, PassThrough } = require( 'stream' )
|
||||
|
||||
module.exports = ( app ) => {
|
||||
|
||||
app.options( '/api/getobjects/:streamId', cors() )
|
||||
|
||||
app.post( '/api/getobjects/:streamId', cors(), contextMiddleware, matomoMiddleware, async ( req, res ) => {
|
||||
let hasStreamAccess = await validatePermissionsReadStream( req.params.streamId, req )
|
||||
if ( !hasStreamAccess.result ) {
|
||||
@@ -46,6 +46,5 @@ module.exports = ( app ) => {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
} )
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict'
|
||||
const zlib = require( 'zlib' )
|
||||
const cors = require( 'cors' )
|
||||
const Busboy = require( 'busboy' )
|
||||
const debug = require( 'debug' )
|
||||
const appRoot = require( 'app-root-path' )
|
||||
@@ -11,7 +12,9 @@ const { validatePermissionsWriteStream } = require( './authUtils' )
|
||||
const { hasObjects } = require( '../services/objects' )
|
||||
|
||||
module.exports = ( app ) => {
|
||||
app.post( '/api/diff/:streamId', contextMiddleware, matomoMiddleware, async ( req, res ) => {
|
||||
app.options( '/api/diff/:streamId', cors() )
|
||||
|
||||
app.post( '/api/diff/:streamId', cors(), contextMiddleware, matomoMiddleware, async ( req, res ) => {
|
||||
let hasStreamAccess = await validatePermissionsWriteStream( req.params.streamId, req )
|
||||
if ( !hasStreamAccess.result ) {
|
||||
return res.status( hasStreamAccess.status ).end()
|
||||
|
||||
@@ -14,7 +14,6 @@ const { SpeckleObjectsStream } = require( './speckleObjectsStream' )
|
||||
const { pipeline, PassThrough } = require( 'stream' )
|
||||
|
||||
module.exports = ( app ) => {
|
||||
|
||||
app.options( '/objects/:streamId/:objectId', cors() )
|
||||
|
||||
app.get( '/objects/:streamId/:objectId', cors(), contextMiddleware, matomoMiddleware, async ( req, res ) => {
|
||||
@@ -54,7 +53,6 @@ module.exports = ( app ) => {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
} )
|
||||
|
||||
app.options( '/objects/:streamId/:objectId/single', cors() )
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict'
|
||||
const zlib = require( 'zlib' )
|
||||
const cors = require( 'cors' )
|
||||
const Busboy = require( 'busboy' )
|
||||
const debug = require( 'debug' )
|
||||
const appRoot = require( 'app-root-path' )
|
||||
@@ -13,7 +14,9 @@ const { createObjects, createObjectsBatched } = require( '../services/objects' )
|
||||
const MAX_FILE_SIZE = 50 * 1024 * 1024
|
||||
|
||||
module.exports = ( app ) => {
|
||||
app.post( '/objects/:streamId', contextMiddleware, matomoMiddleware, async ( req, res ) => {
|
||||
app.options( '/objects/:streamId', cors() )
|
||||
|
||||
app.post( '/objects/:streamId', cors(), contextMiddleware, matomoMiddleware, async ( req, res ) => {
|
||||
let hasStreamAccess = await validatePermissionsWriteStream( req.params.streamId, req )
|
||||
if ( !hasStreamAccess.result ) {
|
||||
return res.status( hasStreamAccess.status ).end()
|
||||
|
||||
@@ -18,6 +18,8 @@ module.exports = {
|
||||
branch.name = name.toLowerCase( )
|
||||
branch.description = description
|
||||
|
||||
if(name) module.exports.validateBranchName( { name } )
|
||||
|
||||
let [ id ] = await Branches( ).returning( 'id' ).insert( branch )
|
||||
|
||||
// update stream updated at
|
||||
@@ -27,9 +29,14 @@ module.exports = {
|
||||
},
|
||||
|
||||
async updateBranch( { id, name, description } ) {
|
||||
if ( name ) module.exports.validateBranchName( { name } )
|
||||
return await Branches( ).where( { id: id } ).update( { name: name ? name.toLowerCase( ) : name, description: description } )
|
||||
},
|
||||
|
||||
validateBranchName( { name } ) {
|
||||
if ( name.startsWith( '/' ) || name.startsWith( '#' ) ) throw new Error( 'Branch names cannot start with # or /.' )
|
||||
},
|
||||
|
||||
async getBranchById( { id } ) {
|
||||
return await Branches( ).where( { id: id } ).first( ).select( '*' )
|
||||
},
|
||||
@@ -54,7 +61,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
async getBranchByNameAndStreamId( { streamId, name } ) {
|
||||
let query = Branches( ).select( '*' ).where( { streamId: streamId } ).andWhere( knex.raw( 'LOWER(name) = ?', [name]) ).first( )
|
||||
let query = Branches( ).select( '*' ).where( { streamId: streamId } ).andWhere( knex.raw( 'LOWER(name) = ?', [ name.toLowerCase() ] ) ).first( )
|
||||
return await query
|
||||
},
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ module.exports = {
|
||||
async createStream( { name, description, isPublic, ownerId } ) {
|
||||
let stream = {
|
||||
id: crs( { length: 10 } ),
|
||||
name: name || 'Random Stream',
|
||||
name: name || generateStreamName(),
|
||||
description: description || '',
|
||||
isPublic: isPublic !== false,
|
||||
updatedAt: knex.fn.now( )
|
||||
@@ -170,3 +170,15 @@ module.exports = {
|
||||
return await query
|
||||
}
|
||||
}
|
||||
|
||||
const adjectives = [
|
||||
'Tall', 'Curved', 'Stacked', 'Purple', 'Pink', 'Rectangular', 'Circular', 'Oval', 'Shiny', 'Speckled', 'Blue', 'Stretched', 'Round', 'Spherical', 'Majestic', 'Symmetrical'
|
||||
]
|
||||
|
||||
const nouns = [
|
||||
'Building', 'House', 'Treehouse', 'Tower', 'Tunnel', 'Bridge', 'Pyramid', 'Structure', 'Edifice', 'Palace', 'Castle', 'Villa'
|
||||
]
|
||||
|
||||
const generateStreamName = () => {
|
||||
return `${adjectives[Math.floor( Math.random()*adjectives.length )]} ${nouns[Math.floor( Math.random()*nouns.length )]}`
|
||||
}
|
||||
|
||||
@@ -11,6 +11,14 @@ const Acl = ( ) => knex( 'server_acl' )
|
||||
const debug = require( 'debug' )
|
||||
const { deleteStream } = require( './streams' )
|
||||
|
||||
|
||||
const changeUserRole = async ( { userId, role } ) => await Acl().where( { userId: userId } ).update( { role:role } )
|
||||
|
||||
const countAdminUsers = async ( ) => {
|
||||
let [ { count } ] = await Acl( ).where( { role: 'server:admin' } ).count( )
|
||||
return parseInt ( count )
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
/*
|
||||
@@ -20,9 +28,8 @@ module.exports = {
|
||||
*/
|
||||
|
||||
async createUser( user ) {
|
||||
let [ { count } ] = await Acl( ).where( { role: 'server:admin' } ).count( )
|
||||
|
||||
user.id = crs( { length: 10 } )
|
||||
user.email = user.email.toLowerCase()
|
||||
|
||||
if ( user.password ) {
|
||||
if ( user.password.length < 8 ) throw new Error( 'Password to short; needs to be 8 characters or longer.' )
|
||||
@@ -34,12 +41,10 @@ module.exports = {
|
||||
if ( usr ) throw new Error( 'Email taken. Try logging in?' )
|
||||
|
||||
let res = await Users( ).returning( 'id' ).insert( user )
|
||||
|
||||
let userRole = await countAdminUsers () === 0 ? 'server:admin' : 'server:user'
|
||||
|
||||
if ( parseInt( count ) === 0 ) {
|
||||
await Acl( ).insert( { userId: res[ 0 ], role: 'server:admin' } )
|
||||
} else {
|
||||
await Acl( ).insert( { userId: res[ 0 ], role: 'server:user' } )
|
||||
}
|
||||
await Acl( ).insert( { userId: res[ 0 ], role: userRole } )
|
||||
|
||||
let loggedUser = { ...user }
|
||||
delete loggedUser.passwordDigest
|
||||
@@ -60,7 +65,6 @@ module.exports = {
|
||||
let existingUser = await Users( ).select( 'id' ).where( { email: user.email } ).first( )
|
||||
|
||||
if ( existingUser ) {
|
||||
|
||||
if ( user.suuid ) {
|
||||
await module.exports.updateUser( existingUser.id, { suuid: user.suuid } )
|
||||
}
|
||||
@@ -90,7 +94,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
async getUserByEmail( { email } ) {
|
||||
let user = await Users( ).where( { email: email } ).select( '*' ).first( )
|
||||
let user = await Users( ).where( { email: email.toLowerCase() } ).select( '*' ).first( )
|
||||
if ( !user ) return null
|
||||
delete user.passwordDigest
|
||||
return user
|
||||
@@ -135,11 +139,12 @@ module.exports = {
|
||||
},
|
||||
|
||||
async validatePasssword( { email, password } ) {
|
||||
let { passwordDigest } = await Users( ).where( { email: email } ).select( 'passwordDigest' ).first( )
|
||||
let { passwordDigest } = await Users( ).where( { email: email.toLowerCase() } ).select( 'passwordDigest' ).first( )
|
||||
return bcrypt.compare( password, passwordDigest )
|
||||
},
|
||||
|
||||
async deleteUser( id ) {
|
||||
//TODO: check for the last admin user to survive
|
||||
debug( 'speckle:db' )( 'Deleting user ' + id )
|
||||
let streams = await knex.raw(
|
||||
`
|
||||
@@ -167,5 +172,57 @@ module.exports = {
|
||||
}
|
||||
|
||||
return await Users( ).where( { id: id } ).del( )
|
||||
},
|
||||
|
||||
async getUsers ( limit = 10, offset = 0, searchQuery = null ) {
|
||||
// sanitize limit
|
||||
const maxLimit = 200
|
||||
if ( limit > maxLimit ) limit = maxLimit
|
||||
|
||||
let query = Users ( )
|
||||
|
||||
if ( searchQuery ) {
|
||||
query.where( queryBuilder => {
|
||||
queryBuilder
|
||||
.where( 'email', 'ILIKE', `%${searchQuery}%` )
|
||||
.orWhere( 'name', 'ILIKE', `%${searchQuery}%` )
|
||||
.orWhere( 'company', 'ILIKE', `%${searchQuery}%` )
|
||||
} )
|
||||
}
|
||||
let users = await query.limit( limit ).offset( offset )
|
||||
users.map( user => delete user.passwordDigest )
|
||||
return users
|
||||
},
|
||||
|
||||
async makeUserAdmin( { userId } ){
|
||||
await changeUserRole( { userId, role:'server:admin' } )
|
||||
},
|
||||
|
||||
async unmakeUserAdmin( { userId } ){
|
||||
// dont delete last admin role
|
||||
if ( await countAdminUsers() === 1 ){
|
||||
let currentAdmin = await Acl( ).where( { role: 'server:admin' } ).first()
|
||||
if ( currentAdmin.userId == userId ) {
|
||||
throw new Error( 'Cannot remove the last admin role from the server' )
|
||||
}
|
||||
}
|
||||
|
||||
await changeUserRole( { userId, role:'server:user' } )
|
||||
},
|
||||
|
||||
|
||||
async countUsers ( searchQuery=null ){
|
||||
let query = Users()
|
||||
if ( searchQuery ) {
|
||||
query.where( queryBuilder => {
|
||||
queryBuilder
|
||||
.where( 'email', 'ILIKE', `%${searchQuery}%` )
|
||||
.orWhere( 'name', 'ILIKE', `%${searchQuery}%` )
|
||||
.orWhere( 'company', 'ILIKE', `%${searchQuery}%` )
|
||||
} )
|
||||
}
|
||||
|
||||
let [ userCount ] = await query.count()
|
||||
return parseInt( userCount.count )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ const {
|
||||
} = require( '../services/branches' )
|
||||
|
||||
describe( 'Branches @core-branches', ( ) => {
|
||||
|
||||
let user = {
|
||||
name: 'Dimitrie Stefanescu',
|
||||
email: 'didimitrie4342@gmail.com',
|
||||
@@ -73,6 +72,52 @@ describe( 'Branches @core-branches', ( ) => {
|
||||
}
|
||||
} )
|
||||
|
||||
it( 'Should not allow branch names starting with # or /', async ( ) => {
|
||||
try {
|
||||
await createBranch( { name: '/pasta', streamId: stream.id, authorId: user.id } )
|
||||
assert.fail( 'Illegal branch name passed through.' )
|
||||
} catch ( err ) {
|
||||
expect( err.message ).to.contain( 'names cannot start with # or /' )
|
||||
}
|
||||
|
||||
try {
|
||||
await createBranch( { name: '#rice', streamId: stream.id, authorId: user.id } )
|
||||
assert.fail( 'Illegal branch name passed through.' )
|
||||
} catch ( err ) {
|
||||
expect( err.message ).to.contain( 'names cannot start with # or /' )
|
||||
}
|
||||
|
||||
try {
|
||||
await updateBranch( { id: branch.id, name: '/super/part/two' } )
|
||||
assert.fail( 'Illegal branch name passed through in update operation.' )
|
||||
} catch ( err ) {
|
||||
expect( err.message ).to.contain( 'names cannot start with # or /' )
|
||||
}
|
||||
|
||||
try {
|
||||
await updateBranch( { id: branch.id, name: '#super#part#three' } )
|
||||
assert.fail( 'Illegal branch name passed through in update operation.' )
|
||||
} catch ( err ) {
|
||||
expect( err.message ).to.contain( 'names cannot start with # or /' )
|
||||
}
|
||||
} )
|
||||
|
||||
it( 'Branch names should be case insensitive (always lowercase)', async ( ) => {
|
||||
let id = await createBranch( { name: 'CaseSensitive', streamId: stream.id, authorId: user.id } )
|
||||
|
||||
let b = await getBranchByNameAndStreamId( { streamId: stream.id, name:'casesensitive' } )
|
||||
expect( b.name ).to.equal( 'casesensitive' )
|
||||
|
||||
let bb = await getBranchByNameAndStreamId( { streamId: stream.id, name:'CaseSensitive' } )
|
||||
expect( bb.name ).to.equal( 'casesensitive' )
|
||||
|
||||
let bbb = await getBranchByNameAndStreamId( { streamId: stream.id, name:'CASESENSITIVE' } )
|
||||
expect( bbb.name ).to.equal( 'casesensitive' )
|
||||
|
||||
// cleanup
|
||||
await deleteBranchById( { id, streamId: stream.id } )
|
||||
} )
|
||||
|
||||
it( 'Should get a branch', async ( ) => {
|
||||
let myBranch = await getBranchById( { id: branch.id } )
|
||||
expect( myBranch.authorId ).to.equal( user.id )
|
||||
@@ -87,7 +132,6 @@ describe( 'Branches @core-branches', ( ) => {
|
||||
} )
|
||||
|
||||
it( 'Should get all stream branches', async ( ) => {
|
||||
|
||||
await createBranch( { name: 'main-faster', streamId: stream.id, authorId: user.id } )
|
||||
await createBranch( { name: 'main-blaster', streamId: stream.id, authorId: user.id } )
|
||||
await createBranch( { name: 'blaster-farter', streamId: stream.id, authorId: user.id } )
|
||||
@@ -112,7 +156,5 @@ describe( 'Branches @core-branches', ( ) => {
|
||||
} catch ( e ){
|
||||
// pass
|
||||
}
|
||||
|
||||
} )
|
||||
|
||||
} )
|
||||
|
||||
@@ -137,6 +137,29 @@ describe( 'GraphQL API Core @core-api', ( ) => {
|
||||
} )
|
||||
} )
|
||||
|
||||
describe ( 'User role change', () => {
|
||||
it ( 'User role is changed', async () => {
|
||||
let queriedUserB = await sendRequest( userA.token, { query: ` { user(id:"${userB.id}") { id name email role } }` } )
|
||||
expect( queriedUserB.body.data.user.role ).to.equal( 'server:user' )
|
||||
let query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "server:admin"})}`
|
||||
await sendRequest( userA.token, { query } )
|
||||
queriedUserB = await sendRequest( userA.token, { query: ` { user(id:"${userB.id}") { id name email role } }` } )
|
||||
expect( queriedUserB.body.data.user.role ).to.equal( 'server:admin' )
|
||||
expect( queriedUserB.body.data )
|
||||
query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "server:user"})}`
|
||||
await sendRequest( userA.token, { query } )
|
||||
queriedUserB = await sendRequest( userA.token, { query: ` { user(id:"${userB.id}") { id name email role } }` } )
|
||||
expect( queriedUserB.body.data.user.role ).to.equal( 'server:user' )
|
||||
} )
|
||||
|
||||
it ( 'Onyl admins can change user role', async () => {
|
||||
let query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "server:admin"})}`
|
||||
let res = await sendRequest( userB.token, { query } )
|
||||
let queriedUserB = await sendRequest( userA.token, { query: ` { user(id:"${userB.id}") { id name email role } }` } )
|
||||
expect( res.body.errors ).to.exist
|
||||
expect( queriedUserB.body.data.user.role ).to.equal( 'server:user' )
|
||||
} )
|
||||
} )
|
||||
describe( 'Streams', ( ) => {
|
||||
it( 'Should create some streams', async ( ) => {
|
||||
const resS1 = await sendRequest( userA.token, { query: 'mutation { streamCreate(stream: { name: "TS1 (u A) Private", description: "Hello World", isPublic:false } ) }' } )
|
||||
@@ -635,16 +658,29 @@ describe( 'GraphQL API Core @core-api', ( ) => {
|
||||
} )
|
||||
|
||||
it( 'Should not search for some users if bad request', async ( ) => {
|
||||
const query_lim = 'query { userSearch( query: "mi" ) { cursor items { id name } } } '
|
||||
let res = await sendRequest( userB.token, { query: query_lim } )
|
||||
const queryLim = 'query { userSearch( query: "mi" ) { cursor items { id name } } } '
|
||||
let res = await sendRequest( userB.token, { query: queryLim } )
|
||||
expect( res ).to.be.json
|
||||
expect( res.body.errors ).to.exist
|
||||
|
||||
const query_pagination = 'query { userSearch( query: "matteo", limit: 200 ) { cursor items { id name } } } '
|
||||
res = await sendRequest( userB.token, { query: query_pagination } )
|
||||
const queryPagination = 'query { userSearch( query: "matteo", limit: 200 ) { cursor items { id name } } } '
|
||||
res = await sendRequest( userB.token, { query: queryPagination } )
|
||||
expect( res ).to.be.json
|
||||
expect( res.body.errors ).to.exist
|
||||
} )
|
||||
|
||||
it ( 'Query users', async () => {
|
||||
const queryUsers = 'query { users( limit: 2, query: "matteo") {totalCount, items {id name}}}'
|
||||
let res = await sendRequest( userA.token, { query: queryUsers } )
|
||||
expect( res ).to.be.json
|
||||
expect( res.body.errors ).to.not.exist
|
||||
expect( res.body.data.users.items.length ).to.equal( 2 )
|
||||
expect( res.body.data.users.totalCount ).to.equal( 10 )
|
||||
|
||||
res = await sendRequest( userC.token, { query: queryUsers } )
|
||||
expect( res ).to.be.json
|
||||
expect( res.body.errors ).to.exist
|
||||
} )
|
||||
} )
|
||||
|
||||
describe( 'Streams', ( ) => {
|
||||
|
||||
@@ -7,11 +7,12 @@ const appRoot = require( 'app-root-path' )
|
||||
const { init } = require( `${appRoot}/app` )
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
chai.use( chaiHttp )
|
||||
|
||||
const knex = require( `${appRoot}/db/knex` )
|
||||
|
||||
const { createUser, findOrCreateUser, getUser, searchUsers, updateUser, deleteUser, validatePasssword, updateUserPassword } = require( '../services/users' )
|
||||
const { createUser, findOrCreateUser, getUser, getUserByEmail, getUsers, searchUsers, countUsers, updateUser, deleteUser, validatePasssword, updateUserPassword, getUserRole, unmakeUserAdmin, makeUserAdmin } = require( '../services/users' )
|
||||
const { createPersonalAccessToken, createAppToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../services/tokens' )
|
||||
const { grantPermissionsStream, createStream, getStream } = require( '../services/streams' )
|
||||
|
||||
@@ -30,7 +31,7 @@ const {
|
||||
|
||||
const { createObject, createObjects } = require( '../services/objects' )
|
||||
|
||||
describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
describe( 'Actors & Tokens @user-services', () => {
|
||||
let myTestActor = {
|
||||
name: 'Dimitrie Stefanescu',
|
||||
email: 'didimitrie@gmail.com',
|
||||
@@ -39,28 +40,22 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
|
||||
let otherUser = {}
|
||||
|
||||
before( async ( ) => {
|
||||
await knex.migrate.rollback( )
|
||||
await knex.migrate.latest( )
|
||||
before( async () => {
|
||||
await knex.migrate.rollback()
|
||||
await knex.migrate.latest()
|
||||
await init()
|
||||
|
||||
let actorId = await createUser( myTestActor )
|
||||
myTestActor.id = actorId
|
||||
|
||||
} )
|
||||
|
||||
after( async ( ) => {
|
||||
await knex.migrate.rollback( )
|
||||
after( async () => {
|
||||
await knex.migrate.rollback()
|
||||
} )
|
||||
|
||||
|
||||
describe( 'Users @core-users', ( ) => {
|
||||
|
||||
it( 'First created user should be a server admin', async ( ) => {
|
||||
|
||||
} )
|
||||
|
||||
it( 'Should create an user', async ( ) => {
|
||||
describe( 'Users @core-users', () => {
|
||||
it( 'Should create an user', async () => {
|
||||
let newUser = { ...myTestActor }
|
||||
newUser.name = 'Bill Gates'
|
||||
newUser.email = 'bill@gates.com'
|
||||
@@ -73,6 +68,24 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
expect( actorId ).to.be.a( 'string' )
|
||||
} )
|
||||
|
||||
it( 'Should store user email lowercase', async () => {
|
||||
let user = { name: 'Marty McFly', email: 'Marty@Mc.Fly', password: 'something_future_proof' }
|
||||
|
||||
let userId = await createUser( user )
|
||||
|
||||
let storedUser = await getUser( userId )
|
||||
expect( storedUser.email ).to.equal( user.email.toLowerCase() )
|
||||
} )
|
||||
|
||||
it( 'Get user by should ignore email casing', async () => {
|
||||
let user = await getUserByEmail( { email: 'BiLL@GaTES.cOm' } )
|
||||
expect( user.email ).to.equal( 'bill@gates.com' )
|
||||
} )
|
||||
|
||||
it( 'Validate password should ignore email casing', async () => {
|
||||
expect( await validatePasssword( { email: 'BiLL@GaTES.cOm', password: 'testthebest' } ) )
|
||||
} )
|
||||
|
||||
it( 'Should not create a user with a too small password', async () => {
|
||||
try {
|
||||
await createUser( { name: 'Dim Sum', email: 'dim@gmail.com', password: '1234567' } )
|
||||
@@ -82,9 +95,8 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
assert.fail( 'short pwd' )
|
||||
} )
|
||||
|
||||
it( 'Should not create an user with the same email', async ( ) => {
|
||||
|
||||
let newUser = { }
|
||||
it( 'Should not create an user with the same email', async () => {
|
||||
let newUser = {}
|
||||
newUser.name = 'Bill Gates'
|
||||
newUser.email = 'bill@gates.com'
|
||||
newUser.password = 'testthebest'
|
||||
@@ -99,9 +111,8 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
|
||||
let ballmerUserId = null
|
||||
|
||||
it( 'Find or create should create a user', async ( ) => {
|
||||
|
||||
let newUser = { }
|
||||
it( 'Find or create should create a user', async () => {
|
||||
let newUser = {}
|
||||
newUser.name = 'Steve Ballmer Balls'
|
||||
newUser.email = 'ballmer@balls.com'
|
||||
newUser.password = 'testthebest'
|
||||
@@ -109,12 +120,10 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
let { id } = await findOrCreateUser( { user: newUser } )
|
||||
ballmerUserId = id
|
||||
expect( id ).to.be.a( 'string' )
|
||||
|
||||
} )
|
||||
|
||||
it( 'Find or create should NOT create a user', async ( ) => {
|
||||
|
||||
let newUser = { }
|
||||
it( 'Find or create should NOT create a user', async () => {
|
||||
let newUser = {}
|
||||
newUser.name = 'Steve Ballmer Balls'
|
||||
newUser.email = 'ballmer@balls.com'
|
||||
newUser.password = 'testthebest'
|
||||
@@ -122,19 +131,18 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
|
||||
let { id } = await findOrCreateUser( { user: newUser } )
|
||||
expect( id ).to.equal( ballmerUserId )
|
||||
|
||||
} )
|
||||
|
||||
// Note: deletion is more complicated.
|
||||
it( 'Should delete a user', async ( ) => {
|
||||
it( 'Should delete a user', async () => {
|
||||
let soloOwnerStream = { name: 'Test Stream 01', description: 'wonderful test stream', isPublic: true }
|
||||
let multiOwnerStream = { name: 'Test Stream 02', description: 'another test stream', isPublic: true }
|
||||
|
||||
soloOwnerStream.id = await createStream( { ...soloOwnerStream, ownerId: ballmerUserId } )
|
||||
multiOwnerStream.id = await createStream( { ...multiOwnerStream, ownerId: ballmerUserId } )
|
||||
|
||||
|
||||
await grantPermissionsStream( { streamId: multiOwnerStream.id, userId: myTestActor.id, role: 'stream:owner' } )
|
||||
|
||||
|
||||
// create a branch for ballmer on the multiowner stream
|
||||
let branch = { name: 'ballmer/dev' }
|
||||
branch.id = await createBranch( { ...branch, streamId: multiOwnerStream.id, authorId: ballmerUserId } )
|
||||
@@ -144,8 +152,8 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
|
||||
// create an object and a commit around it on the multiowner stream
|
||||
let objId = await createObject( multiOwnerStream.id, { pie: 'in the sky' } )
|
||||
let commitId = await createCommitByBranchName( { streamId: multiOwnerStream.id, branchName: 'ballmer/dev', message: 'breakfast commit', sourceApplication: 'tests', objectId:objId, authorId: ballmerUserId } )
|
||||
|
||||
let commitId = await createCommitByBranchName( { streamId: multiOwnerStream.id, branchName: 'ballmer/dev', message: 'breakfast commit', sourceApplication: 'tests', objectId: objId, authorId: ballmerUserId } )
|
||||
|
||||
await deleteUser( ballmerUserId )
|
||||
|
||||
if ( await getStream( { streamId: soloOwnerStream.id } ) !== undefined ) {
|
||||
@@ -160,7 +168,7 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
let branches = await getBranchesByStreamId( { streamId: multiOwnerStream.id } )
|
||||
expect( branches.items.length ).to.equal( 3 )
|
||||
|
||||
let branchCommits = await getCommitsByBranchName( { streamId: multiOwnerStream.id, branchName:'ballmer/dev' } )
|
||||
let branchCommits = await getCommitsByBranchName( { streamId: multiOwnerStream.id, branchName: 'ballmer/dev' } )
|
||||
expect( branchCommits.commits.length ).to.equal( 1 )
|
||||
|
||||
let commit = await getCommitById( { id: commitId } )
|
||||
@@ -174,18 +182,18 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
assert.fail( 'user not deleted' )
|
||||
} )
|
||||
|
||||
it( 'Should get a user', async ( ) => {
|
||||
it( 'Should get a user', async () => {
|
||||
let actor = await getUser( myTestActor.id )
|
||||
expect( actor ).to.not.have.property( 'passwordDigest' )
|
||||
} )
|
||||
|
||||
it( 'Should search and get users', async ( ) => {
|
||||
it( 'Should search and get users', async () => {
|
||||
let { users } = await searchUsers( 'gates', 20, null )
|
||||
expect( users ).to.have.lengthOf( 1 )
|
||||
expect( users[ 0 ].name ).to.equal( 'Bill Gates' )
|
||||
expect( users[0].name ).to.equal( 'Bill Gates' )
|
||||
} )
|
||||
|
||||
it( 'Should update a user', async ( ) => {
|
||||
it( 'Should update a user', async () => {
|
||||
let updatedActor = { ...myTestActor }
|
||||
updatedActor.name = 'didimitrie'
|
||||
|
||||
@@ -193,10 +201,9 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
|
||||
let actor = await getUser( myTestActor.id )
|
||||
expect( actor.name ).to.equal( updatedActor.name )
|
||||
|
||||
} )
|
||||
|
||||
it( 'Should not update password', async ( ) => {
|
||||
it( 'Should not update password', async () => {
|
||||
let updatedActor = { ...myTestActor }
|
||||
updatedActor.password = 'failwhale'
|
||||
|
||||
@@ -206,7 +213,7 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
expect( match ).to.equal( false )
|
||||
} )
|
||||
|
||||
it( 'Should validate user password', async ( ) => {
|
||||
it( 'Should validate user password', async () => {
|
||||
let actor = {}
|
||||
actor.password = 'super-test-200'
|
||||
actor.email = 'e@ma.il'
|
||||
@@ -218,11 +225,10 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
expect( match ).to.equal( true )
|
||||
let match_wrong = await validatePasssword( { email: actor.email, password: 'super-test-2000' } )
|
||||
expect( match_wrong ).to.equal( false )
|
||||
|
||||
} )
|
||||
|
||||
it( 'Should update the password of a user', async() => {
|
||||
let id = await createUser( { name: 'D', email:'tester@mcbester.com', password:'H4!b5at+kWls-8yh4Guq' } ) // https://mostsecure.pw
|
||||
it( 'Should update the password of a user', async () => {
|
||||
let id = await createUser( { name: 'D', email: 'tester@mcbester.com', password: 'H4!b5at+kWls-8yh4Guq' } ) // https://mostsecure.pw
|
||||
await updateUserPassword( { id, newPassword: 'Hello Dogs and Cats' } )
|
||||
|
||||
let match = await validatePasssword( { email: 'tester@mcbester.com', password: 'Hello Dogs and Cats' } )
|
||||
@@ -230,21 +236,21 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
} )
|
||||
} )
|
||||
|
||||
describe( 'API Tokens @core-apitokens', ( ) => {
|
||||
describe( 'API Tokens @core-apitokens', () => {
|
||||
let myFirstToken
|
||||
let pregeneratedToken
|
||||
let revokedToken
|
||||
let someOtherToken
|
||||
let expireSoonToken
|
||||
|
||||
before( async ( ) => {
|
||||
before( async () => {
|
||||
pregeneratedToken = await createPersonalAccessToken( myTestActor.id, 'Whabadub', [ 'streams:read', 'streams:write', 'profile:read', 'users:email' ] )
|
||||
revokedToken = await createPersonalAccessToken( myTestActor.id, 'Mr. Revoked', [ 'streams:read' ] )
|
||||
someOtherToken = await createPersonalAccessToken( otherUser.id, 'Hello World', [ 'streams:write' ] )
|
||||
expireSoonToken = await createPersonalAccessToken( myTestActor.id, 'Mayfly', [ 'streams:read' ], 1 ) // 1ms lifespan
|
||||
} )
|
||||
|
||||
it( 'Should create an personal api token', async ( ) => {
|
||||
it( 'Should create an personal api token', async () => {
|
||||
let scopes = [ 'streams:write', 'profile:read' ]
|
||||
let name = 'My Test Token'
|
||||
|
||||
@@ -257,7 +263,7 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
// expect( test ).to.have.lengthOf( 42 )
|
||||
// } )
|
||||
|
||||
it( 'Should validate a token', async ( ) => {
|
||||
it( 'Should validate a token', async () => {
|
||||
let res = await validateToken( pregeneratedToken )
|
||||
expect( res ).to.have.property( 'valid' )
|
||||
expect( res.valid ).to.equal( true )
|
||||
@@ -266,25 +272,130 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
expect( res ).to.have.property( 'role' )
|
||||
} )
|
||||
|
||||
it( 'Should revoke an api token', async ( ) => {
|
||||
it( 'Should revoke an api token', async () => {
|
||||
await revokeToken( revokedToken, myTestActor.id )
|
||||
let res = await validateToken( revokedToken )
|
||||
expect( res ).to.have.property( 'valid' )
|
||||
expect( res.valid ).to.equal( false )
|
||||
} )
|
||||
|
||||
it( 'Should refuse an expired token', async ( ) => {
|
||||
it( 'Should refuse an expired token', async () => {
|
||||
let res = await validateToken( expireSoonToken )
|
||||
expect( res.valid ).to.equal( false )
|
||||
// assert.fail( )
|
||||
} )
|
||||
|
||||
it( 'Should get the tokens of an user', async ( ) => {
|
||||
it( 'Should get the tokens of an user', async () => {
|
||||
let userTokens = await getUserTokens( myTestActor.id )
|
||||
expect( userTokens ).to.be.an( 'array' )
|
||||
expect( userTokens ).to.have.lengthOf( 2 )
|
||||
} )
|
||||
} )
|
||||
|
||||
|
||||
} )
|
||||
|
||||
|
||||
describe( 'User admin @user-services', () => {
|
||||
let myTestActor = {
|
||||
name: 'Gergo Jedlicska',
|
||||
email: 'gergo@jedlicska.com',
|
||||
password: 'sn3aky-1337-b1m'
|
||||
}
|
||||
|
||||
before( async () => {
|
||||
await knex.migrate.rollback()
|
||||
await knex.migrate.latest()
|
||||
await init()
|
||||
|
||||
let actorId = await createUser( myTestActor )
|
||||
myTestActor.id = actorId
|
||||
} )
|
||||
|
||||
after( async () => {
|
||||
await knex.migrate.rollback()
|
||||
} )
|
||||
|
||||
it( 'First created user should be admin', async () => {
|
||||
let users = await getUsers( 100, 0 )
|
||||
expect( users ).to.be.an( 'array' )
|
||||
expect( users ).to.have.lengthOf( 1 )
|
||||
let firstUser = users[0]
|
||||
|
||||
let userRole = await getUserRole( firstUser.id )
|
||||
expect( userRole ).to.equal( 'server:admin' )
|
||||
} )
|
||||
|
||||
it( 'Count user knows how to count', async () => {
|
||||
expect( await countUsers() ).to.equal( 1 )
|
||||
let newUser = { ...myTestActor }
|
||||
newUser.name = 'Bill Gates'
|
||||
newUser.email = 'bill@gates.com'
|
||||
newUser.password = 'testthebest'
|
||||
|
||||
let actorId = await createUser( newUser )
|
||||
|
||||
expect( await countUsers() ).to.equal( 2 )
|
||||
|
||||
await deleteUser( actorId )
|
||||
expect( await countUsers() ).to.equal( 1 )
|
||||
} )
|
||||
|
||||
it( 'Get users query limit is sanitized to upper limit', async () => {
|
||||
let createNewDroid = ( number ) => {
|
||||
return {
|
||||
name: `${number}`,
|
||||
email: `${number}@droidarmy.com`,
|
||||
password: 'sn3aky-1337-b1m'
|
||||
}
|
||||
}
|
||||
|
||||
let userInputs = Array( 250 ).fill().map( ( v, i ) => createNewDroid( i ) )
|
||||
|
||||
expect( await countUsers() ).to.equal( 1 )
|
||||
|
||||
await Promise.all( userInputs.map( userInput => createUser( userInput ) ) )
|
||||
expect( await countUsers() ).to.equal( 251 )
|
||||
|
||||
let users = await getUsers( 2000000 )
|
||||
expect( users ).to.have.lengthOf( 200 )
|
||||
} )
|
||||
|
||||
it( 'Get users offset is applied', async () => {
|
||||
let users = await getUsers( 200, 200 )
|
||||
expect( users ).to.have.lengthOf( 51 )
|
||||
} )
|
||||
|
||||
it( 'User query filters', async () => {
|
||||
let users = await getUsers( 100, 0, 'gergo' )
|
||||
expect( users ).to.have.lengthOf( 1 )
|
||||
let [ user ] = users
|
||||
expect( user.email ).to.equal( 'gergo@jedlicska.com' )
|
||||
} )
|
||||
|
||||
it( 'Count users applies query', async () => {
|
||||
expect( await countUsers( 'droid' ) ).to.equal( 250 )
|
||||
} )
|
||||
|
||||
it( 'Change user role modifies role', async () => {
|
||||
let [ user ] = await getUsers( 1, 10 )
|
||||
|
||||
let oldRole = await getUserRole( user.id )
|
||||
expect( oldRole ).to.equal( 'server:user' )
|
||||
|
||||
await makeUserAdmin( { userId: user.id } )
|
||||
let newRole = await getUserRole( user.id )
|
||||
expect( newRole ).to.equal( 'server:admin' )
|
||||
|
||||
await unmakeUserAdmin( { userId: user.id } )
|
||||
newRole = await getUserRole( user.id )
|
||||
expect( newRole ).to.equal( 'server:user' )
|
||||
} )
|
||||
|
||||
it( 'Ensure at least one admin remains in the server', async () => {
|
||||
try {
|
||||
await unmakeUserAdmin( { userId: myTestActor.id, role: 'server:admin' } )
|
||||
assert.fail( 'This should have failed' )
|
||||
} catch ( err ) {
|
||||
expect( err.message ).to.equal( 'Cannot remove the last admin role from the server' )
|
||||
}
|
||||
} )
|
||||
} )
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
const { getStreamFileUploads, getFileInfo } = require( '../../services/fileuploads' )
|
||||
|
||||
module.exports = {
|
||||
Stream: {
|
||||
async fileUploads( parent, args, context, info ) {
|
||||
return await getStreamFileUploads( { streamId:parent.id } )
|
||||
},
|
||||
async fileUpload( parent, args, context, info ) {
|
||||
return await getFileInfo( { fileId: args.id } )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
extend type Stream {
|
||||
"""
|
||||
Returns a list of all the file uploads for this stream.
|
||||
"""
|
||||
fileUploads: [FileUpload]
|
||||
"""
|
||||
Returns a specific file upload that belongs to this stream.
|
||||
"""
|
||||
fileUpload(id:String!): FileUpload
|
||||
}
|
||||
|
||||
type FileUpload {
|
||||
id: String!
|
||||
streamId: String!
|
||||
branchName: String
|
||||
"""
|
||||
If present, the conversion result is stored in this commit.
|
||||
"""
|
||||
convertedCommitId: String
|
||||
"""
|
||||
The user's id that uploaded this file.
|
||||
"""
|
||||
userId: String!
|
||||
"""
|
||||
0 = queued, 1 = processing, 2 = success, 3 = error
|
||||
"""
|
||||
convertedStatus: Int!
|
||||
"""
|
||||
Holds any errors or info.
|
||||
"""
|
||||
convertedMessage: String
|
||||
fileName: String!
|
||||
fileType: String!
|
||||
fileSize: Int!
|
||||
uploadComplete: Boolean!
|
||||
uploadDate: DateTime!
|
||||
convertedLastUpdate: DateTime!
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/* istanbul ignore file */
|
||||
'use strict'
|
||||
|
||||
const debug = require( 'debug' )
|
||||
const express = require( 'express' )
|
||||
const appRoot = require( 'app-root-path' )
|
||||
const Busboy = require( 'busboy' )
|
||||
|
||||
const cors = require( 'cors' )
|
||||
const { matomoMiddleware } = require( `${appRoot}/logging/matomoHelper` )
|
||||
const { contextMiddleware, validateScopes, authorizeResolver } = require( `${appRoot}/modules/shared` )
|
||||
|
||||
const { checkBucket, uploadFile, getFileInfo, getFileStream } = require( './services/fileuploads' )
|
||||
const { getStream } = require ( '../core/services/streams' )
|
||||
|
||||
exports.init = async ( app, options ) => {
|
||||
if ( process.env.DISABLE_FILE_UPLOADS ) {
|
||||
debug( 'speckle:modules' )( '📄 FileUploads module is DISABLED' )
|
||||
return
|
||||
} else {
|
||||
debug( 'speckle:modules' )( '📄 Init FileUploads module' )
|
||||
}
|
||||
|
||||
if ( !process.env.S3_BUCKET ) {
|
||||
debug( 'speckle:modules' )( 'ERROR: S3_BUCKET env variable was not specified. File uploads will be DISABLED.' )
|
||||
return
|
||||
}
|
||||
|
||||
await checkBucket()
|
||||
|
||||
|
||||
let checkStreamPermissions = async ( req ) => {
|
||||
if ( !req.context || !req.context.auth ) {
|
||||
return { hasPermissions: false, httpErrorCode: 401 }
|
||||
}
|
||||
|
||||
try {
|
||||
await validateScopes( req.context.scopes, 'streams:write' )
|
||||
} catch ( err ) {
|
||||
return { hasPermissions: false, httpErrorCode: 401 }
|
||||
}
|
||||
|
||||
try {
|
||||
await authorizeResolver( req.context.userId, req.params.streamId, 'stream:contributor' )
|
||||
} catch ( err ) {
|
||||
return { hasPermissions: false, httpErrorCode: 401 }
|
||||
}
|
||||
|
||||
return { hasPermissions: true, httpErrorCode: 200 }
|
||||
}
|
||||
|
||||
app.get( '/api/file/:fileId', contextMiddleware, matomoMiddleware, async ( req, res ) => {
|
||||
if ( process.env.DISABLE_FILE_UPLOADS ) {
|
||||
return res.status( 503 ).send( 'File uploads are disabled on this server' )
|
||||
}
|
||||
|
||||
let fileInfo = await getFileInfo( { fileId: req.params.fileId } )
|
||||
|
||||
if ( !fileInfo )
|
||||
return res.status( 404 ).send( 'File not found' )
|
||||
|
||||
// Check stream read access
|
||||
let streamId = fileInfo.streamId
|
||||
const stream = await getStream( { streamId: streamId, userId: req.context.userId } )
|
||||
|
||||
if ( !stream ) {
|
||||
return res.status( 404 ).send( 'File stream not found' )
|
||||
}
|
||||
|
||||
if ( !stream.isPublic && req.context.auth === false ) {
|
||||
return res.status( 401 ).send( 'You must be logged in to access private streams' )
|
||||
}
|
||||
|
||||
if ( !stream.isPublic ) {
|
||||
try {
|
||||
await validateScopes( req.context.scopes, 'streams:read' )
|
||||
} catch ( err ) {
|
||||
return res.status( 401 ).send( 'The provided auth token can\'t read streams' )
|
||||
}
|
||||
|
||||
try {
|
||||
await authorizeResolver( req.context.userId, streamId, 'stream:reviewer' )
|
||||
} catch ( err ) {
|
||||
return res.status( 401 ).send( 'You don\'t have access to this private stream' )
|
||||
}
|
||||
}
|
||||
|
||||
let fileStream = await getFileStream( { fileId: req.params.fileId } )
|
||||
|
||||
res.writeHead( 200, { 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename="${fileInfo.fileName}"`, } )
|
||||
|
||||
fileStream.pipe( res )
|
||||
} ),
|
||||
|
||||
app.post( '/api/file/:fileType/:streamId/:branchName?', contextMiddleware, matomoMiddleware, async ( req, res ) => {
|
||||
if ( process.env.DISABLE_FILE_UPLOADS ) {
|
||||
return res.status( 503 ).send( 'File uploads are disabled on this server' )
|
||||
}
|
||||
let { hasPermissions, httpErrorCode } = await checkStreamPermissions( req )
|
||||
if ( !hasPermissions ) {
|
||||
return res.status( httpErrorCode ).end()
|
||||
}
|
||||
|
||||
let fileUploadPromises = []
|
||||
let busboy = new Busboy( { headers: req.headers } )
|
||||
|
||||
busboy.on( 'file', function( fieldname, file, filename, encoding, mimetype ) {
|
||||
let promise = uploadFile( {
|
||||
streamId: req.params.streamId,
|
||||
branchName: req.params.branchName || '',
|
||||
userId: req.context.userId,
|
||||
fileName: filename,
|
||||
fileType: req.params.fileType,
|
||||
fileStream: file
|
||||
} )
|
||||
fileUploadPromises.push( promise )
|
||||
} )
|
||||
|
||||
busboy.on( 'finish', async function() {
|
||||
let fileIds = []
|
||||
|
||||
for ( let promise of fileUploadPromises )
|
||||
{
|
||||
let fileId = await promise
|
||||
fileIds.push( fileId )
|
||||
}
|
||||
res.send( fileIds )
|
||||
} )
|
||||
req.pipe( busboy )
|
||||
} )
|
||||
}
|
||||
|
||||
exports.finalize = () => {}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* istanbul ignore file */
|
||||
'use strict'
|
||||
|
||||
exports.up = async knex => {
|
||||
await knex.schema.createTable( 'file_uploads', table => {
|
||||
table.string( 'id' ).primary( )
|
||||
table.string( 'streamId', 10 ).references( 'id' ).inTable( 'streams' ).onDelete( 'cascade' )
|
||||
table.string( 'branchName' )
|
||||
table.string( 'userId' )
|
||||
table.string( 'fileName' ).notNullable( )
|
||||
table.string( 'fileType' ).notNullable( )
|
||||
table.integer( 'fileSize' )
|
||||
table.boolean( 'uploadComplete' ).notNullable( ).defaultTo( false )
|
||||
table.timestamp( 'uploadDate' ).notNullable( ).defaultTo( knex.fn.now( ) )
|
||||
// 0 = queued, 1 = in progress, 2 = success, 3 = error
|
||||
table.integer( 'convertedStatus' ).notNullable( ).defaultTo( 0 )
|
||||
table.timestamp( 'convertedLastUpdate' ).notNullable( ).defaultTo( knex.fn.now( ) )
|
||||
table.string( 'convertedMessage' )
|
||||
table.string( 'convertedCommitId' )
|
||||
|
||||
table.index( [ 'streamId' ] )
|
||||
} )
|
||||
}
|
||||
|
||||
exports.down = async knex => {
|
||||
await knex.schema.dropTableIfExists( 'file_uploads' )
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/* istanbul ignore file */
|
||||
'use strict'
|
||||
|
||||
const appRoot = require( 'app-root-path' )
|
||||
const crs = require( 'crypto-random-string' )
|
||||
const knex = require( `${appRoot}/db/knex` )
|
||||
const S3 = require( 'aws-sdk/clients/s3' )
|
||||
const stream = require( 'stream' )
|
||||
|
||||
const FileUploads = ( ) => knex( 'file_uploads' )
|
||||
|
||||
function getS3Config()
|
||||
{
|
||||
return {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY || 'minioadmin',
|
||||
secretAccessKey: process.env.S3_SECRET_KEY || 'minioadmin',
|
||||
endpoint: process.env.S3_ENDPOINT || 'http://127.0.0.1:9000' ,
|
||||
s3ForcePathStyle: true,
|
||||
signatureVersion: 'v4'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
|
||||
async checkBucket() {
|
||||
const s3 = new S3( getS3Config() )
|
||||
let Bucket = process.env.S3_BUCKET
|
||||
|
||||
try {
|
||||
let data = await s3.headBucket( { Bucket } ).promise()
|
||||
return
|
||||
} catch ( err ) {
|
||||
if ( err.statusCode === 403 ) {
|
||||
throw new Error( 'Access denied to S3 bucket ' )
|
||||
}
|
||||
if ( process.env.S3_CREATE_BUCKET === 'true' ) {
|
||||
await s3.createBucket( { Bucket } ).promise()
|
||||
} else {
|
||||
throw new Error( `Can't open S3 bucket '${Bucket}': ${err.toString()}` )
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async getFileInfo( { fileId } ) {
|
||||
let fileInfo = await FileUploads( ).where( { id: fileId } ).select( '*' ).first( )
|
||||
return fileInfo
|
||||
},
|
||||
|
||||
async getStreamFileUploads( { streamId } ) {
|
||||
let fileInfos = await FileUploads().where( { streamId: streamId } ).select( '*' ).orderBy( [ { column: 'uploadDate', order: 'desc' } ] )
|
||||
return fileInfos
|
||||
},
|
||||
|
||||
async getFileStream( { fileId } ) {
|
||||
const s3 = new S3( getS3Config() )
|
||||
let Bucket = process.env.S3_BUCKET
|
||||
let Key = `files/${fileId}`
|
||||
|
||||
let fileStream = s3.getObject( { Key, Bucket } ).createReadStream()
|
||||
return fileStream
|
||||
},
|
||||
|
||||
async uploadFile( { streamId, branchName, userId, fileName, fileType, fileStream } ) {
|
||||
// Create ID and db entry
|
||||
let fileId = crs( { length: 10 } )
|
||||
let dbFile = {
|
||||
id: fileId,
|
||||
streamId,
|
||||
branchName,
|
||||
userId,
|
||||
fileName,
|
||||
fileType,
|
||||
}
|
||||
await FileUploads( ).insert( dbFile )
|
||||
|
||||
// Upload stream
|
||||
const s3 = new S3( getS3Config() )
|
||||
let Bucket = process.env.S3_BUCKET
|
||||
// TODO: error if missing
|
||||
let Key = `files/${fileId}`
|
||||
|
||||
let uploadResponse = await s3.upload( { Bucket, Key, Body: fileStream } ).promise()
|
||||
|
||||
// Get file size and update db entry
|
||||
let headResponse = await s3.headObject( { Key, Bucket } ).promise()
|
||||
let fileSize = headResponse.ContentLength
|
||||
|
||||
await FileUploads().where( { id: fileId } ).update( { uploadComplete: true, fileSize } )
|
||||
|
||||
return fileId
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ const { scalarResolvers, scalarSchemas } = require( './core/graph/scalars' )
|
||||
exports.init = async ( app ) => {
|
||||
let dirs = fs.readdirSync( `${appRoot}/modules` )
|
||||
|
||||
let moduleDirs = [ './core', './auth', './apiexplorer', './emails', './pwdreset', './serverinvites', './previews' ] // TODO: add './invites'
|
||||
let moduleDirs = [ './core', './auth', './apiexplorer', './emails', './pwdreset', './serverinvites', './previews', './fileuploads' ] // TODO: add './invites'
|
||||
|
||||
// Stage 1: initialise all modules
|
||||
for ( let dir of moduleDirs ) {
|
||||
|
||||
@@ -16,12 +16,12 @@ const Invites = () => knex( 'server_invites' )
|
||||
module.exports = {
|
||||
async createAndSendInvite( { email, inviterId, message, resourceTarget, resourceId, role } ) {
|
||||
// check if email is already registered as a user
|
||||
email = email.toLowerCase()
|
||||
let existingUser = await getUserByEmail( { email } )
|
||||
|
||||
if ( existingUser ) throw new Error( 'This email is already associated with an account on this server!' )
|
||||
|
||||
if ( message ) {
|
||||
|
||||
if ( message.length >= 1024 ) {
|
||||
throw new Error( 'Personal message too long.' )
|
||||
}
|
||||
@@ -105,12 +105,12 @@ This email was sent from ${serverInfo.name} at ${process.env.CANONICAL_URL}, dep
|
||||
},
|
||||
|
||||
async getInviteByEmail( { email } ) {
|
||||
return await Invites().where( { email: email } ).select( '*' ).first()
|
||||
return await Invites().where( { email: email.toLowerCase() } ).select( '*' ).first()
|
||||
},
|
||||
|
||||
async validateInvite( { email, id } ) {
|
||||
const invite = await module.exports.getInviteById( { id } )
|
||||
return invite && invite.email === email && !invite.used
|
||||
return invite && invite.email === email.toLowerCase() && !invite.used
|
||||
},
|
||||
|
||||
async useInvite( { id, email } ) {
|
||||
@@ -120,7 +120,7 @@ This email was sent from ${serverInfo.name} at ${process.env.CANONICAL_URL}, dep
|
||||
let invite = await module.exports.getInviteById( { id } )
|
||||
if ( !invite ) throw new Error( 'Invite not found' )
|
||||
if ( invite.used ) throw new Error( 'Invite has been used' )
|
||||
if ( invite.email !== email ) throw new Error( 'Invite email mismatch. Please use the original email the invite was sent to register.' )
|
||||
if ( invite.email !== email.toLowerCase() ) throw new Error( 'Invite email mismatch. Please use the original email the invite was sent to register.' )
|
||||
|
||||
if ( invite.resourceId && invite.resourceTarget && invite.role ) {
|
||||
let user = await getUserByEmail( { email: invite.email } )
|
||||
|
||||
@@ -19,7 +19,6 @@ const { createPersonalAccessToken } = require( `${appRoot}/modules/core/services
|
||||
const serverAddress = 'http://localhost:3300'
|
||||
|
||||
describe( 'Server Invites @server-invites', ( ) => {
|
||||
|
||||
let myApp
|
||||
|
||||
describe( 'Services @server-invites-services', () => {
|
||||
@@ -43,15 +42,16 @@ describe( 'Server Invites @server-invites', ( ) => {
|
||||
} )
|
||||
|
||||
it( 'should create an invite', async() => {
|
||||
|
||||
let inviteId = await createAndSendInvite( { email:'didimitrie@gmail.com', inviterId: actor.id, message: 'Hey, join!' } )
|
||||
expect( inviteId ).to.be.a( 'string' )
|
||||
|
||||
} )
|
||||
|
||||
it( 'should store invited email as lowercase', async() => {
|
||||
let inviteId = await createAndSendInvite( { email:'GerGO@gmaIl.com', inviterId: actor.id, message: 'Hey, join!' } )
|
||||
expect( inviteId ).to.be.a( 'string' )
|
||||
} )
|
||||
|
||||
it( 'should not allow multiple invites for the same email', async() => {
|
||||
|
||||
let inviteId = await createAndSendInvite( { email:'cat@speckle.systems', inviterId: actor.id, message: 'Hey, join!' } )
|
||||
|
||||
try {
|
||||
@@ -61,9 +61,11 @@ describe( 'Server Invites @server-invites', ( ) => {
|
||||
}
|
||||
assert.fail( 'should not allow multiple invites for the same email' )
|
||||
} )
|
||||
it( 'low multiple invites for the same email regardles of casing', () => {
|
||||
return createAndSendInvite( { email:'dIdImItrIe@gmaIl.com', inviterId: actor.id, message: 'Hey, join!' } ).then( ( result ) => {} ).catch( ( result ) => { expect( result.message ).to.equal( 'Already invited!' ) } )
|
||||
} )
|
||||
|
||||
it( 'should not allow self invites', async() => {
|
||||
|
||||
try {
|
||||
await createAndSendInvite( { email: 'didimitrie-100@gmail.com', inviterId: actor.id } )
|
||||
} catch ( e ) {
|
||||
@@ -73,7 +75,6 @@ describe( 'Server Invites @server-invites', ( ) => {
|
||||
} )
|
||||
|
||||
it( 'should not allow invites from no user', async() => {
|
||||
|
||||
try {
|
||||
await createAndSendInvite( { email: 'didimitrie233-100@gmail.com', inviterId: 'fake' } )
|
||||
} catch ( e ) {
|
||||
@@ -83,7 +84,6 @@ describe( 'Server Invites @server-invites', ( ) => {
|
||||
} )
|
||||
|
||||
it( 'should not allow invites with a too long message', async() => {
|
||||
|
||||
try {
|
||||
let inviteId = await createAndSendInvite( {
|
||||
email: '123456@gmail.com',
|
||||
@@ -111,7 +111,6 @@ describe( 'Server Invites @server-invites', ( ) => {
|
||||
expect( invite.email ).to.equal( 'badger@speckle.systems' )
|
||||
expect( invite.used ).to.equal( false )
|
||||
expect( invite.inviterId ).to.equal( actor.id )
|
||||
|
||||
} )
|
||||
|
||||
it( 'should get an invite by email', async() => {
|
||||
@@ -127,7 +126,7 @@ describe( 'Server Invites @server-invites', ( ) => {
|
||||
it( 'should validate an invite', async() => {
|
||||
let inviteId = await createAndSendInvite( { email:'raven@speckle.systems', inviterId: actor.id, message: 'Hey, join!' } )
|
||||
|
||||
const valid = await validateInvite( { email: 'raven@speckle.systems', id: inviteId } )
|
||||
const valid = await validateInvite( { email: 'rAvEn@specklE.sYstems', id: inviteId } )
|
||||
const invalid = await validateInvite( { email: 'bunny@speckle.systems', id: inviteId } )
|
||||
|
||||
expect( valid ).to.equal( true )
|
||||
@@ -144,14 +143,14 @@ describe( 'Server Invites @server-invites', ( ) => {
|
||||
// pass
|
||||
}
|
||||
|
||||
let result = await useInvite( { id: inviteId, email:'crow@speckle.systems' } )
|
||||
let result = await useInvite( { id: inviteId, email:'crOw@specKle.systeMs' } )
|
||||
|
||||
let invite = await getInviteByEmail( { email: 'crow@speckle.systems' } )
|
||||
let invite = await getInviteByEmail( { email: 'crow@speCkle.syStems' } )
|
||||
expect( result ).equals( true )
|
||||
expect( invite.used ).equals( true )
|
||||
|
||||
try {
|
||||
await useInvite( { id: inviteId, email:'crow@speckle.systems' } )
|
||||
await useInvite( { id: inviteId, email:'CrOw@speckle.systems' } )
|
||||
assert.fail( 'Should not be able to use an already used invite.' )
|
||||
} catch ( e ) {
|
||||
//pass
|
||||
@@ -209,7 +208,6 @@ describe( 'Server Invites @server-invites', ( ) => {
|
||||
} )
|
||||
|
||||
it( 'should create a server invite', async() => {
|
||||
|
||||
const res = await sendRequest( testToken, {
|
||||
query: 'mutation inviteToServer($input: ServerInviteCreateInput!) { serverInviteCreate( input: $input ) }',
|
||||
variables: { input: { email: 'cabbages@speckle.systems', message: 'wow!' } }
|
||||
@@ -220,7 +218,6 @@ describe( 'Server Invites @server-invites', ( ) => {
|
||||
} )
|
||||
|
||||
it( 'should create a stream invite', async() => {
|
||||
|
||||
let stream = { name: 'test', description:'wow' }
|
||||
stream.id = await createStream( { ...stream, ownerId: actor.id } )
|
||||
|
||||
@@ -232,14 +229,11 @@ describe( 'Server Invites @server-invites', ( ) => {
|
||||
expect( res.body.errors ).to.not.exist
|
||||
expect( res.body.data.streamInviteCreate ).to.equal( true )
|
||||
} )
|
||||
|
||||
} )
|
||||
} )
|
||||
|
||||
function sendRequest( auth, obj, address = serverAddress ) {
|
||||
|
||||
return chai.request( address ).post( '/graphql' ).set( 'Authorization', auth ).send( obj )
|
||||
|
||||
}
|
||||
|
||||
const longInviteMessage =
|
||||
|
||||
@@ -21,10 +21,9 @@ async function contextApiTokenHelper( { req, res, connection } ) {
|
||||
|
||||
if ( connection && connection.context.token ) { // Websockets (subscriptions)
|
||||
token = connection.context.token
|
||||
} else if ( req && req.headers.authorization ) { // Standard http
|
||||
} else if ( req && req.headers.authorization ) { // Standard http post
|
||||
token = req.headers.authorization
|
||||
}
|
||||
|
||||
}
|
||||
if ( token && token.includes( 'Bearer ' ) ) {
|
||||
token = token.split( ' ' )[ 1 ]
|
||||
}
|
||||
|
||||
Generated
+108
-3
@@ -2786,6 +2786,49 @@
|
||||
"resolved": "https://registry.npmjs.org/auto-load/-/auto-load-3.0.4.tgz",
|
||||
"integrity": "sha512-ufENezHsnouUiIgwCMuqzcdiABBucBb8CV/5uchw9XuMhf8KXIqF3PgxRzhIuW3C470gjb5niq6zaaF9nhjPIQ=="
|
||||
},
|
||||
"aws-sdk": {
|
||||
"version": "2.989.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.989.0.tgz",
|
||||
"integrity": "sha512-sMjvqeF9mEOxXkhOAUjCrBt2iYafclkmaIbgSdjJ+te7zKXeReqrc6P3VgIGUxU8kwmdSro0n1NjrXbzKQJhcw==",
|
||||
"requires": {
|
||||
"buffer": "4.9.2",
|
||||
"events": "1.1.1",
|
||||
"ieee754": "1.1.13",
|
||||
"jmespath": "0.15.0",
|
||||
"querystring": "0.2.0",
|
||||
"sax": "1.2.1",
|
||||
"url": "0.10.3",
|
||||
"uuid": "3.3.2",
|
||||
"xml2js": "0.4.19"
|
||||
},
|
||||
"dependencies": {
|
||||
"buffer": {
|
||||
"version": "4.9.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
|
||||
"integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
|
||||
"requires": {
|
||||
"base64-js": "^1.0.2",
|
||||
"ieee754": "^1.1.4",
|
||||
"isarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
|
||||
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
|
||||
"integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
@@ -2796,6 +2839,23 @@
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
|
||||
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"follow-redirects": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"backo2": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
|
||||
@@ -5243,6 +5303,11 @@
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
|
||||
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
|
||||
},
|
||||
"events": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
||||
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
|
||||
},
|
||||
"execa": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||
@@ -7815,6 +7880,11 @@
|
||||
"resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz",
|
||||
"integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg=="
|
||||
},
|
||||
"jmespath": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
|
||||
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -9586,9 +9656,9 @@
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
|
||||
},
|
||||
"object-path": {
|
||||
"version": "0.11.7",
|
||||
"resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.7.tgz",
|
||||
"integrity": "sha512-T4evaK9VfGGQskXBDILcn6F90ZD+WO3OwRFFQ2rmZdUH4vQeDBpiolTpVlPY2yj5xSepyILTjDyM6UvbbdHMZw=="
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz",
|
||||
"integrity": "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA=="
|
||||
},
|
||||
"object-visit": {
|
||||
"version": "1.0.1",
|
||||
@@ -10483,6 +10553,11 @@
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
|
||||
},
|
||||
"querystring": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
|
||||
},
|
||||
"quick-lru": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
|
||||
@@ -12640,6 +12715,22 @@
|
||||
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
|
||||
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
|
||||
},
|
||||
"url": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
|
||||
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
|
||||
"requires": {
|
||||
"punycode": "1.3.2",
|
||||
"querystring": "0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"punycode": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
|
||||
}
|
||||
}
|
||||
},
|
||||
"url-parse-lax": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
|
||||
@@ -13008,6 +13099,20 @@
|
||||
"resolved": "https://registry.npmjs.org/xml-escape/-/xml-escape-1.1.0.tgz",
|
||||
"integrity": "sha1-OQTBQ/qOs6ADDsZG0pAqLxtwbEQ="
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.19",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
|
||||
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~9.0.1"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "9.0.7",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
|
||||
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
|
||||
},
|
||||
"xss": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"apollo-server-testing": "^2.19.0",
|
||||
"app-root-path": "^3.0.0",
|
||||
"auto-load": "^3.0.4",
|
||||
"aws-sdk": "^2.989.0",
|
||||
"bcrypt": "^5.0.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"busboy": "^0.3.1",
|
||||
@@ -73,6 +74,7 @@
|
||||
"apollo-link": "^1.2.14",
|
||||
"apollo-link-http": "^1.5.17",
|
||||
"apollo-link-ws": "^1.0.20",
|
||||
"axios": "^0.21.4",
|
||||
"chai": "^4.2.0",
|
||||
"chai-http": "^4.3.0",
|
||||
"concurrently": "^5.2.0",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const appRoot = require( 'app-root-path' )
|
||||
const { createUser } = require( `${appRoot}/modules/core/services/users` )
|
||||
const https = require( 'https' )
|
||||
const axios = require( 'axios' )
|
||||
const { fetch } = require( 'node-fetch' )
|
||||
|
||||
const main = async () => {
|
||||
let userInputs = ( await fetch( 'https://randomuser.me/api/?results=250' ) ).json().results.map( user => {
|
||||
return {
|
||||
name: `${user.name.first} ${user.name.last}`,
|
||||
email: user.email,
|
||||
password: `${user.login.password}${user.login.password}`
|
||||
}
|
||||
} )
|
||||
await Promise.all( userInputs.map( userInput => createUser( userInput ) ) )
|
||||
}
|
||||
|
||||
|
||||
main()
|
||||
.then( console.log( 'created' ) )
|
||||
.catch( console.log( 'failed' ) )
|
||||
Generated
+3
-3
@@ -10514,9 +10514,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"tmpl": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
|
||||
"integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=",
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
|
||||
"dev": true
|
||||
},
|
||||
"to-fast-properties": {
|
||||
|
||||
@@ -74,7 +74,7 @@ export default class Coverter {
|
||||
let val = await this.resolveReference( element )
|
||||
if ( !val.units ) val.units = obj.units
|
||||
let { bufferGeometry } = await this.convert( val, scale )
|
||||
callback( new ObjectWrapper( bufferGeometry, { renderMaterial: val.renderMaterial } ) )
|
||||
callback( new ObjectWrapper( bufferGeometry, { renderMaterial: val.renderMaterial, ...obj } ) )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +189,5 @@ export default class Viewer extends EventEmitter {
|
||||
|
||||
dispose() {
|
||||
// TODO
|
||||
}l
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user