Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f85c029765 | |||
| 8aeb882e9e | |||
| f01e0d3e89 | |||
| 47170c0241 | |||
| 4c898679ad | |||
| 809ea7bb8d | |||
| 4995ecf28e | |||
| 99cc44f518 | |||
| 925ece5fa6 | |||
| b547b9e237 | |||
| 208d937698 | |||
| 488deca574 | |||
| cf9112c714 | |||
| f11378e1d2 | |||
| 95be883b16 | |||
| 985064287b | |||
| f5f99ba3b2 | |||
| 22fd03125a | |||
| ce193b41ad | |||
| e3af320f4d | |||
| dfb5fd444a | |||
| ffdb9cd2b5 | |||
| 872c2552c0 | |||
| 56f9f2747a | |||
| 5a407d45f0 | |||
| 89e6706ee4 | |||
| 1dee2e50fb | |||
| 9fba4d6207 | |||
| f1968ac990 | |||
| 5425bfae33 | |||
| c1c48eeb1e | |||
| 2dcb21bbd6 | |||
| 0dc2d8abc6 | |||
| f55d410911 | |||
| 1a5837e28a | |||
| 79d3d15e9e | |||
| cfe0078fc6 | |||
| b9cacf8440 | |||
| 7002f75c6d | |||
| 6d3fd718ba | |||
| 3a361a9e16 | |||
| f751c45cf6 | |||
| 9c7e5f5276 | |||
| e135471d4a | |||
| e345c2aef8 | |||
| a2b523aadc | |||
| 17e484301c | |||
| 7f73317bc4 | |||
| ebc7707b01 | |||
| 11dcaa6bfd | |||
| 5e057b2ed0 | |||
| 288e113d57 | |||
| 6f2d4b0dd5 | |||
| bdb78f2d2f | |||
| 5b902578fc | |||
| 4579ead966 | |||
| 9dc13940eb | |||
| 6ec73b1aad | |||
| 0830c4c818 | |||
| 71d76a239a | |||
| 3170255986 | |||
| dc25e59ada | |||
| 5d82e2ab5f | |||
| f8c91ec062 | |||
| 17d0e93ec7 | |||
| b3bc596e84 | |||
| d0fbb350d3 | |||
| f8c1d9f96e | |||
| 5cb0a1eafd | |||
| ceccc31ec7 | |||
| 1d0798081d | |||
| 5b40aab84c | |||
| 4e8a6015cb | |||
| 8b9d950376 | |||
| 9eef750890 | |||
| 92dd0583f9 | |||
| 2cb1207999 | |||
| 1289a2a842 | |||
| a44fa41bbf |
@@ -1,78 +0,0 @@
|
||||
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 }}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
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
|
||||
@@ -15,7 +15,6 @@ pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Excel Desktop",
|
||||
"type": "msedge",
|
||||
"request": "attach",
|
||||
"useWebView": true,
|
||||
"port": 9229,
|
||||
"timeout": 600000,
|
||||
"webRoot": "${workspaceFolder}/src",
|
||||
"preLaunchTask": "npm: excel",
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///src/*": "${webRoot}/*",
|
||||
"webpack:///src/*.vue": "${webRoot}/*.vue",
|
||||
"webpack:///./src/*.js": "${webRoot}/*.js",
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -19,7 +19,7 @@ Comprehensive developer and user documentation can be found in our:
|
||||
For developing and debugging this connector you'll need to set up a Speckle App.
|
||||
The server on which the app runs must be on `https`, so **do not use a local Speckle server** on `http://localhost:3000/` as it will not work.
|
||||
|
||||
You can use `https://latest.speckle.dev/` or `https://speckle.xyz/`.
|
||||
You can use `https://latest.speckle.systems/` or `https://app.speckle.systems/`.
|
||||
|
||||
Now open up its frontend, and under your profile register a new app.
|
||||
|
||||
|
||||
Generated
+915
-118
File diff suppressed because it is too large
Load Diff
+7
-3
@@ -6,23 +6,27 @@
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"excel": "office-addin-debugging start public/manifest.xml",
|
||||
"excel:web": "office-addin-debugging start public/manifest.xml web --document https://teocomi-my.sharepoint.com/:x:/g/personal/teocomi_teocomi_onmicrosoft_com/EdxKPPFhnMdDoGclr4J-xJAB7H6-TRJ5s5ZnXzVdrdFyUg?e=VAFmBN",
|
||||
"excel:web": "office-addin-debugging start public/manifest.xml web --document https://skfn3-my.sharepoint.com/:x:/r/personal/connorisadmin_skfn3_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7B46999CFC-9B5A-4716-9F71-FECAEA59A6E8%7D",
|
||||
"excel:web2": "office-addin-debugging start public/manifest.xml web --document https://teocomi-my.sharepoint.com/:x:/g/personal/teocomi_teocomi_onmicrosoft_com/EdxKPPFhnMdDoGclr4J-xJABLf-Ao6CJ902W95kcFPL2fA?e=mD0B8a",
|
||||
"excel:prod": "office-addin-debugging start public/manifest-prod.xml",
|
||||
"excel:web-prod": "office-addin-debugging start public/manifest-prod.xml web --document https://teocomi-my.sharepoint.com/:x:/g/personal/teocomi_teocomi_onmicrosoft_com/EdxKPPFhnMdDoGclr4J-xJAB7H6-TRJ5s5ZnXzVdrdFyUg?e=VAFmBN",
|
||||
"stop": "office-addin-debugging stop public/manifest.xml",
|
||||
"validate": "office-toolbox validate -m public/manifest.xml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@speckle/objectloader": "^2.3.0",
|
||||
"@mdi/font": "^7.2.96",
|
||||
"@speckle/objectloader": "^2.9.0",
|
||||
"@speckle/viewer": "^2.14.7",
|
||||
"core-js": "^3.6.5",
|
||||
"crypto-js": "^4.1.1",
|
||||
"flat": "^5.0.2",
|
||||
"v-tooltip": "^2.1.3",
|
||||
"vue": "^2.6.12",
|
||||
"vue-apollo": "^3.0.5",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-mixpanel": "1.0.7",
|
||||
"vue-router": "^3.5.1",
|
||||
"vue-timeago": "^5.1.3",
|
||||
"vue-mixpanel": "1.0.7",
|
||||
"vuetify": "^2.5.0",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persist": "^3.1.3",
|
||||
|
||||
+4
-5
@@ -11,10 +11,6 @@
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
@@ -25,7 +21,10 @@
|
||||
</noscript>
|
||||
<!-- built files will be auto injected -->
|
||||
<script type="text/javascript">
|
||||
if (window.navigator.userAgent.indexOf('Trident') > -1 || window.navigator.userAgent.indexOf('Edge') > -1) {
|
||||
if (
|
||||
window.navigator.userAgent.indexOf('Trident') > -1 ||
|
||||
window.navigator.userAgent.indexOf('Edge') > -1
|
||||
) {
|
||||
//render unsupported message
|
||||
document.write(
|
||||
'<h1><br/>Sorry, this Excel version is not supported as it still uses IE 11/Legacy Edge.<br/>Please update it or use Excel Online.</h1>'
|
||||
|
||||
+2
-2
@@ -22,8 +22,8 @@
|
||||
<SupportUrl DefaultValue="https://speckle.guide/user/excel.html" />
|
||||
|
||||
<AppDomains>
|
||||
<AppDomain>https://latest.speckle.dev</AppDomain>
|
||||
<AppDomain>https://speckle.xyz</AppDomain>
|
||||
<AppDomain>https://latest.speckle.systems</AppDomain>
|
||||
<AppDomain>https://app.speckle.systems</AppDomain>
|
||||
<AppDomain>https://localhost:3000</AppDomain>
|
||||
</AppDomains>
|
||||
|
||||
|
||||
@@ -110,11 +110,6 @@ export default {
|
||||
drawer: null,
|
||||
showSnackbar: false,
|
||||
items: [
|
||||
{
|
||||
name: 'Add stream',
|
||||
icon: '➕',
|
||||
to: '/add'
|
||||
},
|
||||
{
|
||||
name: 'Streams',
|
||||
icon: '📃',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-card class="pa-5 mb-3" style="transition: all 0.2s" @click="addStream">
|
||||
<v-card class="pa-5 mb-3" style="transition: all 0.2s" @click="openStream">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="8" class="align-self-center">
|
||||
<div class="subtitle-1">
|
||||
@@ -47,14 +47,16 @@
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4" class="text-sm-center text-md-right align-self-center">
|
||||
<div>
|
||||
<user-avatar
|
||||
v-for="user in collaboratorsSlice"
|
||||
:id="user.id"
|
||||
:key="user.id"
|
||||
:avatar="user.avatar"
|
||||
:size="30"
|
||||
:name="user.name"
|
||||
/>
|
||||
<span v-for="user in collaboratorsSlice" :key="user.id">
|
||||
<user-avatar
|
||||
v-if="user.id"
|
||||
:id="user.id"
|
||||
:avatar="user.avatar"
|
||||
:size="30"
|
||||
:name="user.name"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div v-if="stream.collaborators.length > collaboratorsSlice.length" class="d-inline">
|
||||
<v-avatar class="ma-1 grey--text text--darken-2" color="grey lighten-3" size="30">
|
||||
<b>+{{ stream.collaborators.length - collaboratorsSlice.length }}</b>
|
||||
@@ -80,22 +82,22 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
collaboratorsSlice() {
|
||||
let limit = 18
|
||||
let limit = 6
|
||||
switch (this.$vuetify.breakpoint.name) {
|
||||
case 'xs':
|
||||
limit = 10
|
||||
limit = 5
|
||||
break
|
||||
case 'sm':
|
||||
limit = 9
|
||||
limit = 5
|
||||
break
|
||||
case 'md':
|
||||
limit = 8
|
||||
limit = 4
|
||||
break
|
||||
case 'lg':
|
||||
limit = 12
|
||||
limit = 6
|
||||
break
|
||||
case 'xl':
|
||||
limit = 18
|
||||
limit = 6
|
||||
break
|
||||
}
|
||||
|
||||
@@ -120,6 +122,9 @@ export default {
|
||||
selectedCommitId: null
|
||||
})
|
||||
this.$router.push('/')
|
||||
},
|
||||
openStream() {
|
||||
this.$router.push(`/streams/${this.stream.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
:stream-id="streamId"
|
||||
:commit-id="commitId"
|
||||
:commit-msg="commitMsg"
|
||||
:nearest-object-id="nearestObjectId"
|
||||
:path-from-nearest-object="`${entry.pathFromNearestObject}`"
|
||||
></component>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="localExpand && currentLimit < value.length">
|
||||
@@ -77,6 +79,14 @@ export default {
|
||||
commitMsg: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
nearestObjectId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
pathFromNearestObject: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -91,13 +101,17 @@ export default {
|
||||
rangeEntries() {
|
||||
let arr = []
|
||||
let index = 0
|
||||
const delimiter = ':::'
|
||||
for (let val of this.range) {
|
||||
index++
|
||||
if (Array.isArray(val)) {
|
||||
arr.push({
|
||||
key: `${index}`,
|
||||
value: val,
|
||||
type: 'ObjectListViewer'
|
||||
type: 'ObjectListViewer',
|
||||
pathFromNearestObject: this.pathFromNearestObject
|
||||
? this.pathFromNearestObject + (index - 1) + delimiter
|
||||
: index - 1 + delimiter
|
||||
})
|
||||
} else if (typeof val === 'object' && val !== null) {
|
||||
if (val.speckle_type && val.speckle_type === 'reference') {
|
||||
@@ -155,8 +169,11 @@ export default {
|
||||
this.commitId,
|
||||
this.commitMsg,
|
||||
this.$refs.modal,
|
||||
ac.signal
|
||||
ac.signal,
|
||||
this.nearestObjectId,
|
||||
this.pathFromNearestObject
|
||||
)
|
||||
|
||||
if (receiverSelection) {
|
||||
receiverSelection.fullKeyName = this.fullKeyName
|
||||
|
||||
|
||||
@@ -77,6 +77,14 @@ export default {
|
||||
commitMsg: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
nearestObjectId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
pathFromNearestObject: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
:stream-id="streamId"
|
||||
:commit-id="commitId"
|
||||
:commit-msg="commitMsg"
|
||||
:nearest-object-id="updatedObjectId"
|
||||
:path-from-nearest-object="`${entry.pathFromNearestObject}`"
|
||||
></component>
|
||||
</v-card-text>
|
||||
<filter-modal ref="modal" />
|
||||
@@ -93,12 +95,22 @@ export default {
|
||||
commitMsg: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
nearestObjectId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
pathFromNearestObject: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localExpand: false,
|
||||
progress: false
|
||||
progress: false,
|
||||
objectEntries: null,
|
||||
updatedObjectId: null
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
@@ -111,6 +123,9 @@ export default {
|
||||
id: this.value.referencedId
|
||||
}
|
||||
},
|
||||
result() {
|
||||
this.objectEntries = this.getObjectEntries()
|
||||
},
|
||||
skip() {
|
||||
return !this.localExpand
|
||||
},
|
||||
@@ -120,11 +135,24 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
objectEntries() {
|
||||
mounted() {
|
||||
this.localExpand = this.expand
|
||||
},
|
||||
methods: {
|
||||
toggleLoadExpand() {
|
||||
this.localExpand = !this.localExpand
|
||||
},
|
||||
cancel() {
|
||||
ac.abort()
|
||||
},
|
||||
getObjectEntries() {
|
||||
if (!this.object) return []
|
||||
let entries = Object.entries(this.object.data)
|
||||
let arr = []
|
||||
this.updatedObjectId = this.object.data.id ?? this.nearestObjectId
|
||||
const delimiter = ':::'
|
||||
console.log(this.updatedObjectId, delimiter)
|
||||
console.log(this.nearestObjectId)
|
||||
for (let [key, val] of entries) {
|
||||
let name = key
|
||||
if (key.startsWith('__')) continue
|
||||
@@ -138,7 +166,7 @@ export default {
|
||||
name,
|
||||
value: val,
|
||||
type: 'ObjectListViewer',
|
||||
description: `List (${val.length} elements)`
|
||||
pathFromNearestObject: key + delimiter
|
||||
})
|
||||
} else if (typeof val === 'object' && val !== null) {
|
||||
if (val.speckle_type && val.speckle_type === 'reference') {
|
||||
@@ -171,17 +199,6 @@ export default {
|
||||
return 0
|
||||
})
|
||||
return arr
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.localExpand = this.expand
|
||||
},
|
||||
methods: {
|
||||
toggleLoadExpand() {
|
||||
this.localExpand = !this.localExpand
|
||||
},
|
||||
cancel() {
|
||||
ac.abort()
|
||||
},
|
||||
async bake() {
|
||||
this.progress = true
|
||||
|
||||
@@ -32,6 +32,10 @@ export default {
|
||||
commitMsg: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
nearestObjectId: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,521 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card v-if="error" class="pa-5 mb-3" style="transition: all 0.2s">
|
||||
<v-card-title class="subtitle-1 px-0 pt-0">
|
||||
{{ error }} ⚠️
|
||||
<div class="floating">
|
||||
<v-btn
|
||||
v-tooltip="`Remove this stream from the document`"
|
||||
small
|
||||
icon
|
||||
color="red"
|
||||
@click="remove"
|
||||
>
|
||||
<v-icon small>mdi-minus-circle-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="`Open this stream in a new window`"
|
||||
small
|
||||
icon
|
||||
color="primary"
|
||||
:href="`${serverUrl}/streams/${savedStream.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon small>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-text class="px-0">
|
||||
<span v-if="error == 'Stream not found'">
|
||||
The stream might have been deleted or belog to another Speckle server
|
||||
</span>
|
||||
<span v-if="error == 'You do not have access to this resource.'">
|
||||
Please ask the stream owner for access or to make it public
|
||||
</span>
|
||||
<br />
|
||||
Stream Id: {{ savedStream.id }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div v-else-if="$apollo.queries.stream.loading" class="mx-0 mb-3">
|
||||
<v-skeleton-loader type="article"></v-skeleton-loader>
|
||||
</div>
|
||||
<v-card v-else-if="stream" class="pa-5 mb-3" style="transition: all 0.2s">
|
||||
<v-row>
|
||||
<v-col class="align-self-center">
|
||||
<div class="subtitle-1">
|
||||
{{ stream.name }}
|
||||
</div>
|
||||
|
||||
<div class="floating">
|
||||
<v-btn
|
||||
v-tooltip="`Remove this stream from the document`"
|
||||
small
|
||||
icon
|
||||
color="red"
|
||||
@click="remove"
|
||||
>
|
||||
<v-icon small>mdi-minus-circle-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="`Open this stream in a new window`"
|
||||
small
|
||||
icon
|
||||
color="primary"
|
||||
:href="`${serverUrl}/streams/${stream.id}/branches/${selectedBranch.name}`"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon small>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="stream.role != 'stream:reviewer'"
|
||||
v-tooltip="`Click to make this a ` + (savedStream.isReceiver ? `sender` : `receiver`)"
|
||||
small
|
||||
icon
|
||||
color="primary"
|
||||
@click="swapReceiver"
|
||||
>
|
||||
<v-icon small>mdi-swap-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="stream-card-select">
|
||||
<v-col cols="6" class="pa-0 align-self-center">
|
||||
<v-select
|
||||
v-if="stream.branches"
|
||||
v-model="selectedBranch"
|
||||
:items="stream.branches.items"
|
||||
item-value="name"
|
||||
solo
|
||||
flat
|
||||
style="width: 100%"
|
||||
dense
|
||||
return-object
|
||||
class="d-inline-block mb-0 pb-0"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<v-icon color="primary" small class="mr-1">mdi-source-branch</v-icon>
|
||||
<span class="text-truncate caption primary--text">{{ item.name }}</span>
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<div class="pa-2">
|
||||
<p class="pa-0 ma-0 caption">{{ item.name }}</p>
|
||||
<p class="caption pa-0 ma-0 grey--text font-weight-light">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="6" class="pa-0 align-self-start">
|
||||
<div v-if="savedStream.isReceiver">
|
||||
<v-select
|
||||
v-if="
|
||||
selectedBranch && selectedBranch.commits && selectedBranch.commits.items.length > 0
|
||||
"
|
||||
v-model="selectedCommit"
|
||||
:items="selectedBranch.commits.items"
|
||||
item-value="id"
|
||||
solo
|
||||
flat
|
||||
dense
|
||||
style="width: 100%"
|
||||
return-object
|
||||
class="d-inline-block mb-0 pb-0"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<v-icon color="primary" small class="mr-1">mdi-source-commit</v-icon>
|
||||
<span class="text-truncate caption primary--text">
|
||||
{{ formatCommitName(item.id) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<div class="pa-2">
|
||||
<p class="pa-0 ma-0 caption">
|
||||
{{ item.id }}
|
||||
<span v-if="formatCommitName(item.id) == 'latest'">(latest)</span>
|
||||
</p>
|
||||
<p class="caption pa-0 ma-0 grey--text font-weight-light">
|
||||
{{ item.message }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
<div v-else class="text-truncate caption mt-2 ml-3">
|
||||
<span>No commits to receive</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="caption d-inline-flex" style="width: 100%">
|
||||
<span
|
||||
v-if="savedStream.selection"
|
||||
v-tooltip="savedStream.selection"
|
||||
class="mt-2 ml-2 text-truncate"
|
||||
>
|
||||
{{ savedStream.selection }}
|
||||
</span>
|
||||
<span v-else class="mt-2 ml-2">No range set</span>
|
||||
|
||||
<v-menu>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
v-tooltip="`Set or clear range`"
|
||||
icon
|
||||
color="primary"
|
||||
small
|
||||
text
|
||||
v-bind="attrs"
|
||||
class="ml-1 mt-1"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon small>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item @click="setRange(true)">
|
||||
<v-list-item-title
|
||||
v-tooltip="`Ranges with headers will be sent as objects`"
|
||||
class="caption"
|
||||
>
|
||||
Set range with headers
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="setRange(false)">
|
||||
<v-list-item-title
|
||||
v-tooltip="`Ranges without headers will be sent as data arrays`"
|
||||
class="caption"
|
||||
>
|
||||
Set range
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="savedStream.selection" @click="clearSelection">
|
||||
<v-list-item-title class="caption">Clear</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="savedStream.isReceiver">
|
||||
<v-col class="align-self-center">
|
||||
<v-dialog v-model="progress" persistent>
|
||||
<v-card class="pt-3">
|
||||
<v-card-text class="caption">
|
||||
Receiving data from the Speckleverse...
|
||||
<v-progress-linear class="mt-2" indeterminate color="primary"></v-progress-linear>
|
||||
<v-btn class="mt-3" outlined x-small color="primary" @click="cancel">Cancel</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-menu top offset-y>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
:disabled="!selectedCommit"
|
||||
color="primary"
|
||||
class="lower"
|
||||
v-bind="attrs"
|
||||
small
|
||||
v-on="on"
|
||||
>
|
||||
<v-img class="mr-2" width="30" height="30" src="../assets/ReceiverWhite@32.png" />
|
||||
|
||||
Receive
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list v-if="selectedCommit" dense>
|
||||
<v-list-item :to="`/streams/${stream.id}/commits/${selectedCommit.id}`">
|
||||
<v-list-item-action class="mr-2">
|
||||
<v-icon small>mdi-filter-variant</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content class="caption">Filter and receive</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item :disabled="!savedStream.receiverSelection" @click="receiveLatest">
|
||||
<v-list-item-action class="mr-2">
|
||||
<v-icon small>mdi-update</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content class="caption">Receive last selection</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<!-- <span v-if="savedStream.receiverSelection">
|
||||
{{ savedStream.receiverSelection.fullKeyName }}
|
||||
</span> -->
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col class="align-self-center d-inline-flex">
|
||||
<v-btn
|
||||
color="primary"
|
||||
small
|
||||
:disabled="!savedStream.selection"
|
||||
class="mt-1"
|
||||
@click="send"
|
||||
>
|
||||
<v-img class="mr-2" width="30" height="30" src="../assets/SenderWhite@32.png" />
|
||||
|
||||
Send
|
||||
</v-btn>
|
||||
|
||||
<v-text-field
|
||||
v-model="message"
|
||||
class="pt-0 mt-0 ml-3 caption"
|
||||
placeholder="Data from Excel"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import streamQuery from '../graphql/stream.gql'
|
||||
import { send, receiveLatest } from '../plugins/excel'
|
||||
import gql from 'graphql-tag'
|
||||
import { createClient } from '../vue-apollo'
|
||||
|
||||
let ac = new AbortController()
|
||||
|
||||
export default {
|
||||
props: {
|
||||
savedStream: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
progress: false,
|
||||
message: ''
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
stream: {
|
||||
prefetch: true,
|
||||
query: streamQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
variables() {
|
||||
return {
|
||||
id: this.savedStream.id
|
||||
}
|
||||
},
|
||||
error(error) {
|
||||
console.log(this.error)
|
||||
this.error = JSON.stringify(error.message)
|
||||
.replaceAll('"', '')
|
||||
.replace('GraphQL error: ', '')
|
||||
console.log(this.error)
|
||||
},
|
||||
skip() {
|
||||
return this.savedStream === null
|
||||
}
|
||||
},
|
||||
$client: createClient(),
|
||||
$subscribe: {
|
||||
streamUpdated: {
|
||||
query: gql`
|
||||
subscription($id: String!) {
|
||||
streamUpdated(streamId: $id)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.savedStream.id }
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
},
|
||||
commitCreated: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
commitCreated(streamId: $streamId)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { streamId: this.savedStream.id }
|
||||
},
|
||||
result(commitInfo) {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
if (this.savedStream.isReceiver)
|
||||
this.$store.dispatch('showSnackbar', {
|
||||
message: `New commit on ${this.stream.name} @ ${commitInfo.data.commitCreated.branchName}`
|
||||
})
|
||||
}
|
||||
},
|
||||
commitUpdated: {
|
||||
query: gql`
|
||||
subscription($id: String!) {
|
||||
commitUpdated(streamId: $id)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.savedStream.id }
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
},
|
||||
branchCreated: {
|
||||
query: gql`
|
||||
subscription($id: String!) {
|
||||
branchCreated(streamId: $id)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.savedStream.id }
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
},
|
||||
branchDeleted: {
|
||||
query: gql`
|
||||
subscription($id: String!) {
|
||||
branchDeleted(streamId: $id)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.savedStream.id }
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
},
|
||||
branchUpdated: {
|
||||
query: gql`
|
||||
subscription($id: String!) {
|
||||
branchUpdated(streamId: $id)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.savedStream.id }
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
serverUrl() {
|
||||
return this.$store.getters.serverUrl
|
||||
},
|
||||
selectedBranch: {
|
||||
get() {
|
||||
if (!this.stream || !this.stream.branches) return null
|
||||
|
||||
let selectedBranchName = this.savedStream.selectedBranchName
|
||||
? this.savedStream.selectedBranchName
|
||||
: 'main'
|
||||
const index = this.stream.branches.items.findIndex((x) => x.name === selectedBranchName)
|
||||
if (index > -1) return this.stream.branches.items[index]
|
||||
return this.stream.branches.items[0]
|
||||
},
|
||||
set(value) {
|
||||
let s = { ...this.savedStream }
|
||||
s.selectedBranchName = value.name
|
||||
this.$store.dispatch('updateStream', s)
|
||||
}
|
||||
},
|
||||
selectedCommit: {
|
||||
get() {
|
||||
if (!this.selectedBranch || !this.selectedBranch.commits) return null
|
||||
|
||||
//not set or latest, return first
|
||||
if (!this.savedStream.selectedCommitId || this.savedStream.selectedCommitId === 'latest')
|
||||
return this.selectedBranch.commits.items[0]
|
||||
|
||||
//try match by id
|
||||
const index = this.selectedBranch.commits.items.findIndex(
|
||||
(x) => x.id === this.savedStream.selectedCommitId
|
||||
)
|
||||
if (index > -1) return this.selectedBranch.commits.items[index]
|
||||
|
||||
return this.selectedBranch.commits.items[0]
|
||||
},
|
||||
set(value) {
|
||||
let s = { ...this.savedStream }
|
||||
const index = this.selectedBranch.commits.items.findIndex((x) => x.id === value.id)
|
||||
s.selectedCommitId = index === 0 ? 'latest' : value.id
|
||||
this.$store.dispatch('updateStream', s)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
swapReceiver() {
|
||||
let s = { ...this.savedStream }
|
||||
s.isReceiver = !s.isReceiver
|
||||
this.$mixpanel.track('Connector Action', { name: 'Stream Swap Receive/Send', type: 'action' })
|
||||
this.$store.dispatch('updateStream', s)
|
||||
},
|
||||
|
||||
async setRange(headers) {
|
||||
await window.Excel.run(async (context) => {
|
||||
let range = context.workbook.getSelectedRange()
|
||||
range.load('address')
|
||||
|
||||
await context.sync()
|
||||
|
||||
let s = { ...this.savedStream }
|
||||
s.selection = range.address
|
||||
s.hasHeaders = headers
|
||||
this.$store.dispatch('updateStream', s)
|
||||
})
|
||||
},
|
||||
clearSelection() {
|
||||
let s = { ...this.savedStream }
|
||||
s.selection = ''
|
||||
this.$store.dispatch('updateStream', s)
|
||||
},
|
||||
|
||||
remove() {
|
||||
this.$mixpanel.track('Connector Action', { name: 'Stream Remove' })
|
||||
return this.$store.dispatch('removeStream', this.savedStream.id)
|
||||
},
|
||||
cancel() {
|
||||
ac.abort()
|
||||
},
|
||||
async send() {
|
||||
this.$mixpanel.track('Send')
|
||||
send(this.savedStream, this.stream.id, this.selectedBranch.name, this.message)
|
||||
},
|
||||
async receiveLatest() {
|
||||
this.$mixpanel.track('Receive')
|
||||
ac = new AbortController()
|
||||
|
||||
console.log(this.savedStream.receiverSelection)
|
||||
|
||||
this.progress = true
|
||||
await receiveLatest(
|
||||
this.selectedCommit.referencedObject,
|
||||
this.stream.id,
|
||||
this.selectedCommit.id,
|
||||
this.selectedCommit.message,
|
||||
this.savedStream.receiverSelection,
|
||||
ac.signal
|
||||
)
|
||||
this.progress = false
|
||||
},
|
||||
formatCommitName(id) {
|
||||
if (this.selectedBranch.commits.items[0].id == id) return 'latest'
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.stream-card-select .v-text-field__details {
|
||||
display: none !important;
|
||||
}
|
||||
.v-btn .lower {
|
||||
text-transform: none;
|
||||
}
|
||||
.floating {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<v-card id="stream-info" class="pa-5 ma-3" style="transition: all 0.2s">
|
||||
<v-row>
|
||||
<v-col class="align-self-center">
|
||||
<div class="subtitle-1">
|
||||
{{ stream.name }}
|
||||
</div>
|
||||
|
||||
<div class="floating">
|
||||
<v-btn
|
||||
v-tooltip="`Open this stream in a new window`"
|
||||
small
|
||||
icon
|
||||
color="primary"
|
||||
:href="`${serverUrl}/streams/${stream.id}/branches/${selectedBranch.name}`"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon small>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="stream.role != 'stream:reviewer'"
|
||||
v-tooltip="`Click to make this a ` + (isReceiver ? `sender` : `receiver`)"
|
||||
small
|
||||
icon
|
||||
color="primary"
|
||||
@click="swapReceiver"
|
||||
>
|
||||
<v-icon small>mdi-swap-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="stream-card-select">
|
||||
<v-col cols="6" class="pa-0 align-self-center">
|
||||
<v-select
|
||||
v-if="stream.branches"
|
||||
v-model="selectedBranch"
|
||||
:items="stream.branches.items"
|
||||
item-value="name"
|
||||
solo
|
||||
flat
|
||||
style="width: 100%"
|
||||
dense
|
||||
return-object
|
||||
class="d-inline-block mb-0 pb-0"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<v-icon color="primary" small class="mr-1">mdi-source-branch</v-icon>
|
||||
<span class="text-truncate caption primary--text">{{ item.name }}</span>
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<div class="pa-2">
|
||||
<p class="pa-0 ma-0 caption">{{ item.name }}</p>
|
||||
<p class="caption pa-0 ma-0 grey--text font-weight-light">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="6" class="pa-0 align-self-start">
|
||||
<div v-if="isReceiver">
|
||||
<v-select
|
||||
v-if="
|
||||
selectedBranch && selectedBranch.commits && selectedBranch.commits.items.length > 0
|
||||
"
|
||||
v-model="selectedCommit"
|
||||
:items="selectedBranch.commits.items"
|
||||
item-value="id"
|
||||
solo
|
||||
flat
|
||||
dense
|
||||
style="width: 100%"
|
||||
return-object
|
||||
class="d-inline-block mb-0 pb-0"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<v-icon color="primary" small class="mr-1">mdi-source-commit</v-icon>
|
||||
<span class="text-truncate caption primary--text">
|
||||
{{ formatCommitName(item.id) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<div class="pa-2">
|
||||
<p class="pa-0 ma-0 caption">
|
||||
{{ item.id }}
|
||||
<span v-if="formatCommitName(item.id) == 'latest'">(latest)</span>
|
||||
</p>
|
||||
<p class="caption pa-0 ma-0 grey--text font-weight-light">
|
||||
{{ item.message }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
<div v-else class="text-truncate caption mt-2 ml-3">
|
||||
<span>No commits to receive</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="caption d-inline-flex" style="width: 100%">
|
||||
<span v-if="selection" v-tooltip="selection" class="mt-2 ml-2 text-truncate">
|
||||
{{ selection }}
|
||||
</span>
|
||||
<span v-else class="mt-2 ml-2">No range set</span>
|
||||
|
||||
<v-menu>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
v-tooltip="`Set or clear range`"
|
||||
icon
|
||||
color="primary"
|
||||
small
|
||||
text
|
||||
v-bind="attrs"
|
||||
class="ml-1 mt-1"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon small>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item @click="setRange(true)">
|
||||
<v-list-item-title
|
||||
v-tooltip="`Ranges with headers will be sent as objects`"
|
||||
class="caption"
|
||||
>
|
||||
Set range with headers
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="setRange(false)">
|
||||
<v-list-item-title
|
||||
v-tooltip="`Ranges without headers will be sent as data arrays`"
|
||||
class="caption"
|
||||
>
|
||||
Set range
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="selection" @click="clearSelection">
|
||||
<v-list-item-title class="caption">Clear</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="isReceiver">
|
||||
<v-col class="align-self-center">
|
||||
<v-dialog v-model="progress" persistent>
|
||||
<v-card class="pt-3">
|
||||
<v-card-text class="caption">
|
||||
Receiving data from the Speckleverse...
|
||||
<v-progress-linear class="mt-2" indeterminate color="primary"></v-progress-linear>
|
||||
<v-btn class="mt-3" outlined x-small color="primary" @click="cancel">Cancel</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-btn
|
||||
v-if="determinedConversion"
|
||||
color="primary"
|
||||
class="lower"
|
||||
v-bind="attrs"
|
||||
small
|
||||
@click="receiveDeterminedConversion"
|
||||
>
|
||||
<v-img class="mr-2" width="30" height="30" src="../assets/ReceiverWhite@32.png" />
|
||||
Receive {{ determinedConversion }}
|
||||
</v-btn>
|
||||
<v-menu v-else top offset-y>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
:disabled="!selectedCommit"
|
||||
color="primary"
|
||||
class="lower"
|
||||
v-bind="attrs"
|
||||
small
|
||||
v-on="on"
|
||||
>
|
||||
<v-img class="mr-2" width="30" height="30" src="../assets/ReceiverWhite@32.png" />
|
||||
Receive {{ determinedConversion }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list v-if="selectedCommit" dense>
|
||||
<!-- <v-list-item :to="`/streams/${stream.id}/commits/${selectedCommit.id}`"> -->
|
||||
<v-list-item @click="filterAndReceive">
|
||||
<v-list-item-action class="mr-2">
|
||||
<v-icon small>mdi-filter-variant</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content class="caption">Filter and receive</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item :disabled="!savedStream.receiverSelection" @click="receiveLatest">
|
||||
<v-list-item-action class="mr-2">
|
||||
<v-icon small>mdi-update</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content class="caption">Receive last selection</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<!-- <span v-if="savedStream.receiverSelection">
|
||||
{{ savedStream.receiverSelection.fullKeyName }}
|
||||
</span> -->
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col class="align-self-center d-inline-flex">
|
||||
<v-btn color="primary" small :disabled="!selection" class="mt-1" @click="send">
|
||||
<v-img class="mr-2" width="30" height="30" src="../assets/SenderWhite@32.png" />
|
||||
|
||||
Send
|
||||
</v-btn>
|
||||
|
||||
<v-text-field
|
||||
v-model="message"
|
||||
class="pt-0 mt-0 ml-3 caption"
|
||||
placeholder="Data from Excel"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import { send, receiveLatest, bakeSchedule } from '../plugins/excel'
|
||||
import { getReferencedObject } from '../plugins/excel'
|
||||
|
||||
let ac = new AbortController()
|
||||
|
||||
export default {
|
||||
props: {
|
||||
stream: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
determinedConversion: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
progress: false,
|
||||
message: '',
|
||||
viewer: null,
|
||||
objectIds: null,
|
||||
selectedBranchName: null,
|
||||
selectedCommitId: null,
|
||||
isReceiver: true,
|
||||
selection: null,
|
||||
hasHeaders: false,
|
||||
receiverSelection: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
serverUrl() {
|
||||
return this.$store.getters.serverUrl
|
||||
},
|
||||
selectedBranch: {
|
||||
get() {
|
||||
if (!this.stream || !this.stream.branches) return null
|
||||
|
||||
let selectedBranchName = this.selectedBranchName ? this.selectedBranchName : 'main'
|
||||
const index = this.stream.branches.items.findIndex((x) => x.name === selectedBranchName)
|
||||
if (index > -1) return this.stream.branches.items[index]
|
||||
return this.stream.branches.items[0]
|
||||
},
|
||||
set(value) {
|
||||
this.selectedBranchName = value.name
|
||||
this.$emit('loadByReferencedId', this.selectedBranch.commits.items[0].referencedObject)
|
||||
this.$store.dispatch('updateStream', this.savedStream)
|
||||
}
|
||||
},
|
||||
selectedCommit: {
|
||||
get() {
|
||||
if (!this.selectedBranch || !this.selectedBranch.commits) return null
|
||||
var commit = null
|
||||
//not set or latest, return first
|
||||
if (!this.selectedCommitId || this.selectedCommitId === 'latest')
|
||||
commit = this.selectedBranch.commits.items[0]
|
||||
//try match by id
|
||||
else {
|
||||
const index = this.selectedBranch.commits.items.findIndex(
|
||||
(x) => x.id === this.selectedCommitId
|
||||
)
|
||||
if (index > -1) commit = this.selectedBranch.commits.items[index]
|
||||
else commit = this.selectedBranch.commits.items[0]
|
||||
}
|
||||
return commit
|
||||
},
|
||||
async set(value) {
|
||||
const index = this.selectedBranch.commits.items.findIndex((x) => x.id === value.id)
|
||||
this.selectedCommitId = value.id
|
||||
this.$emit('loadByReferencedId', this.selectedBranch.commits.items[index].referencedObject)
|
||||
this.$store.dispatch('updateStream', this.savedStream)
|
||||
}
|
||||
},
|
||||
savedStream() {
|
||||
return {
|
||||
id: this.stream.Id,
|
||||
isReceiver: this.isReceiver,
|
||||
selection: this.selection,
|
||||
hasHeaders: this.hasHeaders,
|
||||
selectedBranchName: this.selectedBranchName,
|
||||
selectedCommitId: this.selectedCommitId,
|
||||
receiverSelection: this.receiverSelection
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(function () {
|
||||
this.$emit('loadByReferencedId', this.selectedCommit.referencedObject)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
swapReceiver() {
|
||||
this.isReceiver = !this.isReceiver
|
||||
this.$emit('loadByCommitId', this.selectedCommitId)
|
||||
this.$store.dispatch('updateStream', this.savedStream)
|
||||
this.$mixpanel.track('Connector Action', { name: 'Stream Swap Receive/Send', type: 'action' })
|
||||
},
|
||||
async filterAndReceive() {
|
||||
this.$store.dispatch('addStream', this.savedStream)
|
||||
this.$router.push(`/streams/${this.stream.id}/commits/${this.selectedCommit.id}`)
|
||||
},
|
||||
async setRange(headers) {
|
||||
await window.Excel.run(async (context) => {
|
||||
let range = context.workbook.getSelectedRange()
|
||||
range.load('address')
|
||||
|
||||
await context.sync()
|
||||
|
||||
this.selection = range.address
|
||||
this.hasHeaders = headers
|
||||
this.$store.dispatch('updateStream', this.savedStream)
|
||||
})
|
||||
},
|
||||
clearSelection() {
|
||||
this.selection = ''
|
||||
this.$store.dispatch('updateStream', this.savedStream)
|
||||
},
|
||||
remove() {
|
||||
this.$mixpanel.track('Connector Action', { name: 'Stream Remove' })
|
||||
return this.$store.dispatch('removeStream', this.savedStream.id)
|
||||
},
|
||||
cancel() {
|
||||
ac.abort()
|
||||
},
|
||||
async send() {
|
||||
// these values need to be set to null or the models will not load
|
||||
// when switching back to the receive mode
|
||||
this.viewer = null
|
||||
this.referencedObject = null
|
||||
this.$store.dispatch('addStream', this.savedStream)
|
||||
|
||||
this.$mixpanel.track('Send')
|
||||
send(this.savedStream, this.stream.id, this.selectedBranch.name, this.message)
|
||||
},
|
||||
async receiveDeterminedConversion() {
|
||||
let data = await getReferencedObject(this.stream.id, this.selectedCommit.referencedObject, {})
|
||||
let ac = new AbortController()
|
||||
if (this.determinedConversion == 'Schedule') {
|
||||
this.receiverSelection = await bakeSchedule(
|
||||
data,
|
||||
this.stream.id,
|
||||
this.selectedCommit.id,
|
||||
this.selectedCommit.message,
|
||||
ac.signal,
|
||||
null, // nearestObjectId,
|
||||
null, // pathFromNearestObj,
|
||||
null, // previousHeaders,
|
||||
null // previousRange
|
||||
)
|
||||
} else {
|
||||
this.$emit('clearDeterminedConversion')
|
||||
}
|
||||
|
||||
if (this.receiverSelection) {
|
||||
this.$store.dispatch('setReceiverSelection', {
|
||||
id: this.stream.id,
|
||||
receiverSelection: this.receiverSelection
|
||||
})
|
||||
}
|
||||
},
|
||||
async receiveLatest() {
|
||||
this.$mixpanel.track('Receive')
|
||||
ac = new AbortController()
|
||||
|
||||
console.log(this.receiverSelection)
|
||||
|
||||
this.progress = true
|
||||
await receiveLatest(
|
||||
this.selectedCommit.referencedObject,
|
||||
this.stream.id,
|
||||
this.selectedCommit.id,
|
||||
this.selectedCommit.message,
|
||||
this.receiverSelection,
|
||||
ac.signal
|
||||
)
|
||||
this.progress = false
|
||||
},
|
||||
formatCommitName(id) {
|
||||
if (this.selectedBranch.commits.items[0].id == id) {
|
||||
console.log(id)
|
||||
return 'latest'
|
||||
}
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.stream-card-select .v-text-field__details {
|
||||
display: none !important;
|
||||
}
|
||||
.v-btn .lower {
|
||||
text-transform: none;
|
||||
}
|
||||
.floating {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 10px;
|
||||
}
|
||||
.background-light {
|
||||
background: #8e9eab;
|
||||
background: -webkit-linear-gradient(to top right, #eeeeee, #c8e8ff) !important;
|
||||
background: linear-gradient(to top right, #ffffff, #c8e8ff) !important;
|
||||
}
|
||||
|
||||
.background-dark {
|
||||
background: #141e30;
|
||||
background: -webkit-linear-gradient(to top left, #243b55, #141e30) !important;
|
||||
background: linear-gradient(to top left, #243b55, #141e30) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div style="display: inline-block">
|
||||
<div v-if="otherUser && otherUser.name" style="display: inline-block">
|
||||
<v-menu v-if="loggedIn" offset-x open-on-hover>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-avatar class="ma-1" color="grey lighten-3" :size="size" v-bind="attrs" v-on="on">
|
||||
@@ -14,12 +14,14 @@
|
||||
<v-img v-else :src="`https://robohash.org/` + id + `.png?size=40x40`" />
|
||||
</v-avatar>
|
||||
<br />
|
||||
<b>{{ user.name }}</b>
|
||||
<b>{{ otherUser.name }}</b>
|
||||
<v-divider class="ma-4"></v-divider>
|
||||
{{ user.company }}
|
||||
{{ otherUser.company }}
|
||||
<br />
|
||||
{{
|
||||
user.bio ? user.bio : 'This user prefers to keep an air of mystery around themselves.'
|
||||
otherUser.bio
|
||||
? otherUser.bio
|
||||
: 'This user prefers to keep an air of mystery around themselves.'
|
||||
}}
|
||||
<br />
|
||||
</v-card-text>
|
||||
@@ -58,7 +60,7 @@ export default {
|
||||
},
|
||||
apollo: {
|
||||
$client: createClient(),
|
||||
user: {
|
||||
otherUser: {
|
||||
query: userQuery,
|
||||
variables() {
|
||||
return {
|
||||
@@ -67,6 +69,9 @@ export default {
|
||||
},
|
||||
skip() {
|
||||
return !this.loggedIn
|
||||
},
|
||||
error(error) {
|
||||
console.log('Could not get user', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<!-- DIALOG: Create Branch -->
|
||||
<v-dialog v-model="showCreateBranch">
|
||||
<template #activator="{ on: dialog, attrs }">
|
||||
<v-btn
|
||||
v-tooltip="'Create Branch'"
|
||||
icon
|
||||
x-small
|
||||
class="ml-0 mr-1"
|
||||
v-bind="attrs"
|
||||
v-on="{ ...dialog }"
|
||||
>
|
||||
<v-icon>mdi-plus-circle</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title class="text-h5 mb-1">Create a New Branch</v-card-title>
|
||||
<v-card-subtitle class="py-0 my-0 font-italic">under {{ streamName }} stream</v-card-subtitle>
|
||||
<v-container class="px-6" pb-0>
|
||||
<v-text-field
|
||||
v-model="branchName"
|
||||
xxxclass="small-text-field"
|
||||
hide-details
|
||||
dense
|
||||
flat
|
||||
placeholder="Branch Name"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="description"
|
||||
xxxclass="small-text-field"
|
||||
hide-details
|
||||
dense
|
||||
flat
|
||||
placeholder="Description (Optional)"
|
||||
/>
|
||||
</v-container>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue darken-1" text @click="showCreateBranch = false">Cancel</v-btn>
|
||||
<v-btn :disabled="branchName === ''" color="blue darken-1" text @click="createBranch">
|
||||
Create
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
// import { bus } from '@/main'
|
||||
export default {
|
||||
name: 'CreateBranchDialog',
|
||||
props: {
|
||||
streamId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
streamName: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCreateBranch: false,
|
||||
branchName: '',
|
||||
description: '',
|
||||
defaultDescription: 'Stream created from SketchUp',
|
||||
accountToCreateStream: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loggedIn() {
|
||||
if (!this.$store.state) return false
|
||||
return this.$store.getters.isAuthenticated
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async createBranch() {
|
||||
let res = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation branchCreate($branch: BranchCreateInput!) {
|
||||
branchCreate(branch: $branch)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
branch: {
|
||||
streamId: this.streamId,
|
||||
name: this.branchName,
|
||||
description: this.description === '' ? this.defaultDescription : this.description
|
||||
}
|
||||
}
|
||||
})
|
||||
// bus.$emit(`create-branch-${this.streamId}`, this.branchName)
|
||||
this.showCreateBranch = false
|
||||
this.branchName = ''
|
||||
this.description = ''
|
||||
this.$mixpanel.track('Connector Action', { name: 'Create Branch' })
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-dialog {
|
||||
max-width: 390px;
|
||||
}
|
||||
.v-text-field >>> input {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.v-text-field >>> label {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<v-container fluid class="px-1 pb-0 pt-1">
|
||||
<v-row>
|
||||
<v-col class="center-content">
|
||||
<!-- DIALOG: Create New Stream -->
|
||||
<v-dialog v-model="showCreateNewStream">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn class="ma-2 pa-3" x-small v-bind="attrs" v-on="on">
|
||||
<v-icon dark left>mdi-plus-circle</v-icon>
|
||||
Create New Stream
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">Create a New Stream</v-card-title>
|
||||
<v-container class="px-6" pb-0>
|
||||
<v-text-field
|
||||
v-model="streamName"
|
||||
xxxclass="small-text-field"
|
||||
hide-details
|
||||
dense
|
||||
flat
|
||||
placeholder="Stream Name (Optional)"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="description"
|
||||
xxxclass="small-text-field"
|
||||
hide-details
|
||||
dense
|
||||
flat
|
||||
placeholder="Description (Optional)"
|
||||
/>
|
||||
<v-switch v-model="privateStream" :label="'Private Stream'"></v-switch>
|
||||
</v-container>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue darken-1" text @click="showCreateNewStream = false">Cancel</v-btn>
|
||||
<v-btn color="blue darken-1" text @click="createStream">Create</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- DIALOG: Add a Stream by ID or URL -->
|
||||
<v-dialog v-model="showCreateStreamById">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn class="ma-2 pa-3" x-small min-width="163" v-bind="attrs" v-on="on">
|
||||
<v-icon dark left>mdi-link-plus</v-icon>
|
||||
Add By ID or URL
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">Add a Stream by ID or URL</v-card-title>
|
||||
<v-card-text>Stream IDs and Stream/Branch/Commit URLs are supported.</v-card-text>
|
||||
<v-container class="px-6">
|
||||
<v-text-field
|
||||
v-model="createStreamByIdText"
|
||||
xxxclass="small-text-field"
|
||||
hide-details
|
||||
dense
|
||||
flat
|
||||
placeholder="Stream URL"
|
||||
/>
|
||||
</v-container>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue darken-1" text @click="showCreateStreamById = false">Cancel</v-btn>
|
||||
<v-btn
|
||||
:disabled="createStreamByIdText === ''"
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="getStream"
|
||||
>
|
||||
Add
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import { StreamWrapper } from '@/utils/streamWrapper'
|
||||
|
||||
export default {
|
||||
name: 'CreateStreamDialog',
|
||||
props: {
|
||||
accountId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
serverUrl: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCreateNewStream: false,
|
||||
showCreateStreamById: false,
|
||||
createStreamByIdText: '',
|
||||
privateStream: false,
|
||||
streamName: '',
|
||||
description: '',
|
||||
defaultDescription: 'Stream created from Excel',
|
||||
accountToCreateStream: null,
|
||||
streamId: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loggedIn() {
|
||||
if (!this.$store.state) return false
|
||||
return this.$store.getters.isAuthenticated
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getStream() {
|
||||
try {
|
||||
const streamWrapper = new StreamWrapper(
|
||||
this.createStreamByIdText,
|
||||
this.accountId,
|
||||
this.serverUrl
|
||||
)
|
||||
this.$router.push(`/streams/${streamWrapper.streamId}/${streamWrapper.commitId}`)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
this.showCreateStreamById = false
|
||||
},
|
||||
async createStream() {
|
||||
let res = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation streamCreate($stream: StreamCreateInput!) {
|
||||
streamCreate(stream: $stream)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
stream: {
|
||||
name: this.streamName,
|
||||
description: this.description === '' ? this.defaultDescription : this.description,
|
||||
isPublic: !this.privateStream
|
||||
}
|
||||
}
|
||||
})
|
||||
this.showCreateNewStream = false
|
||||
this.streamName = ''
|
||||
this.description = ''
|
||||
this.$mixpanel.track('Connector Action', { name: 'Create Stream' })
|
||||
this.refresh()
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.center-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.v-dialog {
|
||||
max-width: 390px;
|
||||
}
|
||||
.v-text-field >>> input {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.v-text-field >>> label {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,6 @@
|
||||
query User($id: String!) {
|
||||
user(id: $id) {
|
||||
query LimitedUser($id: String!) {
|
||||
otherUser(id: $id) {
|
||||
id
|
||||
email
|
||||
name
|
||||
bio
|
||||
company
|
||||
|
||||
@@ -4,6 +4,7 @@ import router from './router'
|
||||
import store from './store'
|
||||
import { apolloProvider } from './vue-apollo'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import {
|
||||
bakeTable,
|
||||
bakeArray,
|
||||
hideRowOrColumn,
|
||||
removeNonAlphanumericCharacters,
|
||||
getIndiciesFromRangeAddress
|
||||
} from './excel'
|
||||
export const tableName = 'SpeckleDataTable'
|
||||
|
||||
export function checkIfReceivingDataTable(item) {
|
||||
if (!Array.isArray(item)) {
|
||||
return checkIfSingleDataTable(item)
|
||||
}
|
||||
|
||||
if (item.length < 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
//it's a flat list
|
||||
else if (!Array.isArray(item[0])) {
|
||||
return checkIfSingleDataTable(item[0])
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfSingleDataTable(item) {
|
||||
if (!(item.speckle_type && item.speckle_type.split('.').at(-1) == 'DataTable')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function formatArrayDataForTable(item, arrayData) {
|
||||
// TODO: support receiving multiple tables
|
||||
if (Array.isArray(item)) {
|
||||
item = item[0]
|
||||
}
|
||||
|
||||
arrayData[0].push('SpeckleColumnMetadataRow')
|
||||
for (let i = 0; i < item.columnCount; i++) {
|
||||
arrayData[0].push(JSON.stringify(item.columnMetadata[i]))
|
||||
}
|
||||
|
||||
for (let i = 0; i < item.rowCount; i++) {
|
||||
let row = []
|
||||
row.push(JSON.stringify(item.rowMetadata[i]))
|
||||
row.push(...item.data[i])
|
||||
arrayData.push(row)
|
||||
}
|
||||
}
|
||||
|
||||
export async function bakeDataTable(item, arrayData, context, sheet, rowStart, colStart) {
|
||||
// TODO: support receiving multiple tables
|
||||
if (Array.isArray(item)) {
|
||||
item = item[0]
|
||||
}
|
||||
|
||||
var tableToUpdate = await getExistingTableInLocation(context, sheet, rowStart, colStart)
|
||||
if (tableToUpdate) {
|
||||
let existingAppId = await getSpeckleIdFromTable(tableToUpdate, sheet, context)
|
||||
if (existingAppId != item.applicationId) {
|
||||
throw new Error('Trying receive a datatable where a different datatable already exists')
|
||||
}
|
||||
|
||||
let indicies = await getMetadataIndex(tableToUpdate, sheet, context)
|
||||
if (indicies[0] != -1 && indicies[1] != -1) {
|
||||
rowStart = indicies[0]
|
||||
colStart = indicies[1]
|
||||
|
||||
await cleanUpTableMetadata(tableToUpdate.id, context)
|
||||
tableToUpdate.delete()
|
||||
}
|
||||
}
|
||||
|
||||
// add one to headerRowIndex because we've added the column metadata as a new first row
|
||||
let headerRowIndex = 1
|
||||
if (item.headerRowIndex) {
|
||||
headerRowIndex = item.headerRowIndex + 1
|
||||
}
|
||||
let name = 'DataTable'
|
||||
if (item.name) {
|
||||
name = item.name
|
||||
}
|
||||
hideRowOrColumn(sheet, colStart, rowStart)
|
||||
await bakeArray(arrayData.splice(0, headerRowIndex), rowStart, colStart, context)
|
||||
|
||||
// set table applicationId in the top left cell
|
||||
arrayData[0][0] = `{"id":"","speckle_type":"Objects.Organization.DataTable","applicationId":"${item.applicationId}","totalChildrenCount":0}`
|
||||
await bakeTable(arrayData, context, sheet, name, rowStart, colStart, headerRowIndex)
|
||||
greyOutReadOnlyColumns(
|
||||
item.columnMetadata,
|
||||
rowStart + 1 + headerRowIndex,
|
||||
colStart + 1,
|
||||
arrayData.length - 1,
|
||||
sheet,
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
async function getExistingTableInLocation(context, sheet, rowStart, colStart) {
|
||||
let range = sheet.getRangeByIndexes(rowStart, colStart, 1, 1)
|
||||
var speckleTableContainingRange = await getDataTableContainingRange(range, sheet, context)
|
||||
if (!speckleTableContainingRange) {
|
||||
return null
|
||||
}
|
||||
return speckleTableContainingRange
|
||||
}
|
||||
|
||||
async function getSpeckleIdFromTable(table, sheet, context) {
|
||||
let range = table.getRange()
|
||||
range.load('rowIndex, columnIndex')
|
||||
await context.sync()
|
||||
|
||||
let firstCell = sheet.getRangeByIndexes(range.rowIndex, range.columnIndex, 1, 1)
|
||||
firstCell.load('values')
|
||||
await context.sync()
|
||||
|
||||
let appIdObj = JSON.parse(firstCell.values[0][0])
|
||||
return appIdObj.applicationId
|
||||
}
|
||||
|
||||
async function getMetadataIndex(table, sheet, context) {
|
||||
table.load('id')
|
||||
await context.sync()
|
||||
let tableId = removeNonAlphanumericCharacters(table.id)
|
||||
let speckleRowMeta = sheet.names.getItemOrNullObject(`speckleRowMetadata_${tableId}`)
|
||||
await context.sync()
|
||||
if (speckleRowMeta.isNullObject) {
|
||||
return [-1, -1]
|
||||
}
|
||||
|
||||
let speckleRowMetaRange = speckleRowMeta.getRange()
|
||||
speckleRowMetaRange.load('columnIndex, rowIndex')
|
||||
await context.sync()
|
||||
|
||||
return [speckleRowMetaRange.rowIndex, speckleRowMetaRange.columnIndex]
|
||||
}
|
||||
|
||||
export async function getDataTableContainingRange(range, sheet, context) {
|
||||
let selectedTable = null
|
||||
sheet.tables.load('count')
|
||||
await context.sync()
|
||||
|
||||
for (let i = 0; i < sheet.tables.count; i++) {
|
||||
let table = sheet.tables.getItemAt(i)
|
||||
let tableMetataIndicies = await getMetadataIndex(table, sheet, context)
|
||||
|
||||
if (tableMetataIndicies[0] == -1 && tableMetataIndicies[1] == -1) {
|
||||
continue
|
||||
}
|
||||
|
||||
let tableRange = table.getRange()
|
||||
tableRange.load('address')
|
||||
range.load('address')
|
||||
await context.sync()
|
||||
|
||||
let tableRangeIndicies = getIndiciesFromRangeAddress(tableRange.address)
|
||||
let rangeIndicies = getIndiciesFromRangeAddress(range.address)
|
||||
if (
|
||||
// sorry these indicies don't cooperate very well. Get metadata index return [rowindex, colindex]
|
||||
// while getIndiciesFromRangeAddress return indicies as excel addresses display them [colindex, rowindex, endcolindex, endRowIndex]
|
||||
tableMetataIndicies[0] <= rangeIndicies[1] &&
|
||||
tableMetataIndicies[1] <= rangeIndicies[0] &&
|
||||
tableRangeIndicies[3] >= rangeIndicies[3] &&
|
||||
tableRangeIndicies[2] >= rangeIndicies[2]
|
||||
) {
|
||||
selectedTable = sheet.tables.getItemAt(i)
|
||||
break
|
||||
} else {
|
||||
console.log(
|
||||
"Are you trying to send a data table? Make sure your selection doesn't extended beyond the table"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTable == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isDataTable = await isSpeckleDataTable(selectedTable, sheet, context)
|
||||
if (selectedTable && isDataTable) {
|
||||
return selectedTable
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function isSpeckleDataTable(table, sheet, context) {
|
||||
let tableRange = table.getRange()
|
||||
tableRange.load('rowIndex, columnIndex')
|
||||
await context.sync()
|
||||
// let tableRangeAddress = tableRange.address
|
||||
// let rangeIndicies = getIndiciesFromRangeAddress(tableRangeAddress)
|
||||
// let firstCellRange = sheet.getRangeByIndexes(rangeIndicies[1], rangeIndicies[0], 1, 1)
|
||||
let firstCellRange = sheet.getRangeByIndexes(tableRange.rowIndex, tableRange.columnIndex, 1, 1)
|
||||
|
||||
firstCellRange.load('values')
|
||||
await context.sync()
|
||||
|
||||
if (firstCellRange.values[0][0].includes('Objects.Organization.DataTable')) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function greyOutReadOnlyColumns(
|
||||
columnMetadata,
|
||||
rowStartIndex,
|
||||
colStartIndex,
|
||||
rowCount,
|
||||
sheet,
|
||||
context
|
||||
) {
|
||||
for (let i = 0; i < columnMetadata.length; i++) {
|
||||
if (columnMetadata[i].IsReadOnly) {
|
||||
let range = sheet.getRangeByIndexes(rowStartIndex, colStartIndex + i, rowCount, 1)
|
||||
range.format.fill.color = '#AEAAAA'
|
||||
}
|
||||
}
|
||||
|
||||
await context.sync()
|
||||
}
|
||||
|
||||
export async function BuildDataTableObject(sendingRange, values, table, sheet, context) {
|
||||
let metaRowIndex = await GetColumnMetadataRowIndex(table, sheet, context)
|
||||
let metaColIndex = await GetRowMetadataColumnIndex(table, context)
|
||||
|
||||
let speckleTable = new DataTable()
|
||||
speckleTable.applicationId = await GetTableApplicationId(table, context)
|
||||
|
||||
sendingRange.load('rowIndex, columnIndex, rowCount, columnCount')
|
||||
await context.sync()
|
||||
|
||||
let metaRowRange = sheet.getRangeByIndexes(
|
||||
metaRowIndex,
|
||||
sendingRange.columnIndex,
|
||||
1,
|
||||
sendingRange.columnCount
|
||||
)
|
||||
let metaColumnRange = sheet.getRangeByIndexes(
|
||||
sendingRange.rowIndex,
|
||||
metaColIndex,
|
||||
sendingRange.rowCount,
|
||||
1
|
||||
)
|
||||
|
||||
metaRowRange.load('values')
|
||||
metaColumnRange.load('values')
|
||||
await context.sync()
|
||||
|
||||
var rangeColumnStart = 0
|
||||
if (sendingRange.columnIndex == metaColIndex) {
|
||||
rangeColumnStart = 1
|
||||
}
|
||||
|
||||
for (let i = rangeColumnStart; i < sendingRange.columnCount; i++) {
|
||||
speckleTable.defineColumn(JSON.parse(metaRowRange.values[0][i]))
|
||||
}
|
||||
|
||||
for (let i = 0; i < sendingRange.rowCount; i++) {
|
||||
speckleTable.addRow(JSON.parse(metaColumnRange.values[i][0]), values[i].slice(rangeColumnStart))
|
||||
}
|
||||
|
||||
return speckleTable
|
||||
}
|
||||
|
||||
export async function GetColumnMetadataRowIndex(table, sheet, context) {
|
||||
let tableRange = table.getRange()
|
||||
tableRange.load('columnCount, columnIndex, rowCount, rowIndex')
|
||||
await context.sync()
|
||||
|
||||
let extendedRowIndex = Math.max(0, tableRange.rowIndex - 5)
|
||||
|
||||
let possibleMetadataRowRange = sheet.getRangeByIndexes(
|
||||
extendedRowIndex,
|
||||
tableRange.columnIndex,
|
||||
tableRange.rowIndex - extendedRowIndex,
|
||||
tableRange.columnCount
|
||||
)
|
||||
|
||||
return await findMetadataRowIndex(possibleMetadataRowRange, context)
|
||||
}
|
||||
|
||||
async function findMetadataRowIndex(range, context) {
|
||||
var found = range.findOrNullObject('SpeckleColumnMetadataRow', {
|
||||
completeMatch: false, // Match the whole cell value.
|
||||
matchCase: true, // Don't match case.
|
||||
searchDirection: window.Excel.SearchDirection.forward // Start search at the beginning of the range.
|
||||
})
|
||||
found.load('rowIndex')
|
||||
await context.sync()
|
||||
if (found.isNullObject) {
|
||||
found = range.findOrNullObject('speckle_type', {
|
||||
completeMatch: false, // Match the whole cell value.
|
||||
matchCase: true, // Match case.
|
||||
searchDirection: window.Excel.SearchDirection.forward // Start search at the beginning of the range.
|
||||
})
|
||||
found.load('rowIndex')
|
||||
await context.sync()
|
||||
}
|
||||
|
||||
if (found.isNullObject) {
|
||||
throw new Error('Could not find column metadata')
|
||||
}
|
||||
|
||||
return found.rowIndex
|
||||
}
|
||||
|
||||
export async function GetRowMetadataColumnIndex(table, context) {
|
||||
let tableRange = table.getRange()
|
||||
tableRange.load('columnIndex, rowCount, rowIndex')
|
||||
await context.sync()
|
||||
|
||||
return tableRange.columnIndex
|
||||
}
|
||||
|
||||
async function GetTableApplicationId(table, context) {
|
||||
const headerRange = table.getHeaderRowRange()
|
||||
headerRange.load('values')
|
||||
await context.sync()
|
||||
|
||||
const tableMetadata = JSON.parse(headerRange.values[0][0])
|
||||
if (!tableMetadata.hasOwnProperty('applicationId'))
|
||||
throw new Error('Cannot find TableApplicationId in table header metadata')
|
||||
|
||||
return tableMetadata.applicationId
|
||||
}
|
||||
|
||||
export async function onTableChanged(eventArgs) {
|
||||
if (eventArgs.changeType != 'ColumnDeleted') {
|
||||
return
|
||||
}
|
||||
await window.Excel.run(async (context) => {
|
||||
const tableId = removeNonAlphanumericCharacters(eventArgs.tableId)
|
||||
let sheet = context.workbook.worksheets.getActiveWorksheet()
|
||||
let speckleRowMeta = sheet.names.getItemOrNullObject(`speckleRowMetadata_${tableId}`)
|
||||
await context.sync()
|
||||
if (speckleRowMeta.isNullObject) {
|
||||
console.log('speckle row metadata was null')
|
||||
return
|
||||
}
|
||||
let table = sheet.tables.getItem(eventArgs.tableId)
|
||||
let tableRange = table.getRange()
|
||||
|
||||
let speckleRowMetaRange = speckleRowMeta.getRange()
|
||||
speckleRowMetaRange.load('columnIndex')
|
||||
tableRange.load('columnCount, columnIndex')
|
||||
await context.sync()
|
||||
|
||||
if (tableRange.columnCount == 1 && tableRange.columnIndex == speckleRowMetaRange.columnIndex) {
|
||||
let speckleColumnMeta = sheet.names.getItemOrNullObject(`speckleColumnMetadata_${tableId}`)
|
||||
await deleteMetadata(speckleRowMeta, false, context)
|
||||
await deleteMetadata(speckleColumnMeta, true, context)
|
||||
}
|
||||
})
|
||||
}
|
||||
export async function onTableDeleted(eventArgs) {
|
||||
await window.Excel.run(async (context) => {
|
||||
await cleanUpTableMetadata(eventArgs.tableId, context)
|
||||
})
|
||||
}
|
||||
|
||||
async function cleanUpTableMetadata(originalTableId, context) {
|
||||
const tableId = removeNonAlphanumericCharacters(originalTableId)
|
||||
let sheet = context.workbook.worksheets.getActiveWorksheet()
|
||||
let speckleColumnMeta = sheet.names.getItemOrNullObject(`speckleColumnMetadata_${tableId}`)
|
||||
let speckleRowMeta = sheet.names.getItemOrNullObject(`speckleRowMetadata_${tableId}`)
|
||||
await context.sync()
|
||||
|
||||
// warning: must delete rowMetadata (which is actually a column) before deleting the colMetadata
|
||||
await deleteMetadata(speckleRowMeta, false, context)
|
||||
await deleteMetadata(speckleColumnMeta, true, context)
|
||||
}
|
||||
|
||||
async function deleteMetadata(namedRange, isRow, context) {
|
||||
if (namedRange.isNullObject) {
|
||||
return
|
||||
}
|
||||
|
||||
let speckleMetaRange = namedRange.getRange()
|
||||
|
||||
try {
|
||||
await findMetadataRowIndex(speckleMetaRange, context)
|
||||
} catch {
|
||||
console.log('speckle metadata doesnt exist in this range anymore')
|
||||
return
|
||||
}
|
||||
|
||||
if (isRow) {
|
||||
speckleMetaRange.load('rowHidden')
|
||||
await context.sync()
|
||||
|
||||
if (speckleMetaRange.rowHidden) {
|
||||
speckleMetaRange.getEntireRow().delete('Up')
|
||||
} else {
|
||||
speckleMetaRange.clear('Contents')
|
||||
}
|
||||
} else {
|
||||
speckleMetaRange.load('columnHidden')
|
||||
await context.sync()
|
||||
|
||||
if (speckleMetaRange.columnHidden) {
|
||||
speckleMetaRange.getEntireColumn().delete('Left')
|
||||
} else {
|
||||
speckleMetaRange.clear('Contents')
|
||||
}
|
||||
}
|
||||
|
||||
namedRange.delete()
|
||||
}
|
||||
|
||||
class Base {
|
||||
id
|
||||
totalChildrenCount
|
||||
applicationId
|
||||
}
|
||||
|
||||
export class DataTable extends Base {
|
||||
get columnCount() {
|
||||
return this.columnMetadata.length
|
||||
}
|
||||
get rowCount() {
|
||||
return this.rowMetadata.length
|
||||
}
|
||||
// eslint-disable-next-line camelcase
|
||||
get speckle_type() {
|
||||
return 'Objects.Organization.DataTable'
|
||||
}
|
||||
headerRowIndex
|
||||
columnMetadata = []
|
||||
rowMetadata = []
|
||||
data = []
|
||||
|
||||
addRow(metadata, objects) {
|
||||
if (objects.length != this.columnCount)
|
||||
throw new Error(
|
||||
`object length of ${objects.length} does not match the column count, ${this.columnCount}`
|
||||
)
|
||||
this.rowMetadata.push(metadata)
|
||||
this.data.push(objects)
|
||||
}
|
||||
|
||||
defineColumn(metadata) {
|
||||
this.columnMetadata.push(metadata)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const jsonObj = Object.assign({}, this)
|
||||
const proto = Object.getPrototypeOf(this)
|
||||
for (const key of Object.getOwnPropertyNames(proto)) {
|
||||
const desc = Object.getOwnPropertyDescriptor(proto, key)
|
||||
const hasGetter = desc && typeof desc.get === 'function'
|
||||
if (hasGetter) {
|
||||
jsonObj[key] = this[key]
|
||||
}
|
||||
}
|
||||
return jsonObj
|
||||
}
|
||||
}
|
||||
|
||||
// export function checkIfSendingDataTable(item, arrayData) {
|
||||
|
||||
// }
|
||||
+498
-116
@@ -1,35 +1,57 @@
|
||||
/* eslint-disable no-unreachable */
|
||||
import flatten from 'flat'
|
||||
import store from '../store/index.js'
|
||||
import { MD5, enc } from 'crypto-js'
|
||||
import {
|
||||
checkIfReceivingDataTable,
|
||||
getDataTableContainingRange,
|
||||
bakeDataTable,
|
||||
formatArrayDataForTable,
|
||||
BuildDataTableObject,
|
||||
onTableChanged,
|
||||
onTableDeleted
|
||||
} from './dataTable.js'
|
||||
|
||||
const unflatten = require('flat').unflatten
|
||||
|
||||
let ignoreEndsWithProps = ['id', 'totalChildrenCount']
|
||||
let ignoreEndsWithProps = ['totalChildrenCount', 'elements']
|
||||
|
||||
let streamId, sheet, rowStart, colStart, arrayData, isTabularData
|
||||
let headerIndices = []
|
||||
let displayValues = ['displayValue', '@displayValue', 'displayMesh']
|
||||
let speckleTypesWithGeometry = ['Objects.Geometry']
|
||||
|
||||
let streamId, sheet, rowStart, colStart, arrayData, isTabularData, arrayIdData
|
||||
|
||||
async function flattenData(item, signal) {
|
||||
if (signal.aborted) return
|
||||
if (Array.isArray(item)) {
|
||||
for (let o of item) {
|
||||
if (signal.aborted) return
|
||||
await flattenSingle(o, signal)
|
||||
let localItems = [...item]
|
||||
const batchSize = 35
|
||||
while (localItems.length > 0) {
|
||||
let batch = localItems.splice(0, batchSize)
|
||||
await Promise.all(batch.map((i) => flattenSingle(i, signal)))
|
||||
}
|
||||
} else {
|
||||
await flattenSingle(item, signal)
|
||||
}
|
||||
}
|
||||
|
||||
async function getReferencedObject(reference, signal) {
|
||||
export async function getReferencedObject(
|
||||
streamId,
|
||||
reference,
|
||||
signal,
|
||||
excludeElementsFromConstruction = true
|
||||
) {
|
||||
if (signal.aborted) return
|
||||
|
||||
let excludeProps = ['__closure']
|
||||
if (excludeElementsFromConstruction) excludeProps.push('elements')
|
||||
|
||||
let loader = await store.dispatch('getObject', {
|
||||
streamId: streamId,
|
||||
objectId: reference,
|
||||
options: {
|
||||
fullyTraverseArrays: false,
|
||||
excludeProps: ['displayValue', 'displayMesh', '__closure', 'elements']
|
||||
excludeProps: excludeProps
|
||||
},
|
||||
signal
|
||||
})
|
||||
@@ -39,27 +61,54 @@ async function getReferencedObject(reference, signal) {
|
||||
|
||||
async function flattenSingle(item, signal) {
|
||||
if (item.speckle_type && item.speckle_type == 'reference') {
|
||||
item = await getReferencedObject(item.referencedId, signal)
|
||||
item = await getReferencedObject(streamId, item.referencedId, signal)
|
||||
}
|
||||
|
||||
let flat = flatten(item)
|
||||
let rowData = []
|
||||
let rowIdData = ''
|
||||
for (const [key, value] of Object.entries(flat)) {
|
||||
if (key === null || value === null) continue
|
||||
if (ignoreEndsWithProps.findIndex((x) => key.endsWith(x)) !== -1) continue
|
||||
|
||||
if (key.endsWith('id')) {
|
||||
rowIdData += idsToBeAdded(key, value, flat)
|
||||
continue
|
||||
}
|
||||
if (displayValues.some((v) => key.includes(v))) {
|
||||
continue
|
||||
}
|
||||
let colIndex = arrayData[0].findIndex((x) => x === key)
|
||||
if (colIndex === -1) {
|
||||
colIndex = arrayData[0].length
|
||||
arrayData[0].push(key)
|
||||
}
|
||||
rowData[colIndex] = value
|
||||
rowData[colIndex] = Array.isArray(value) ? JSON.stringify(value) : value
|
||||
}
|
||||
arrayData.push(rowData)
|
||||
arrayIdData.push(rowIdData)
|
||||
}
|
||||
|
||||
function idsToBeAdded(key, value, flat) {
|
||||
for (let i = 0; i < displayValues.length; i++) {
|
||||
if (flat[key.slice(0, -2).concat(displayValues[i]).concat('.0.id')]) {
|
||||
return value + ','
|
||||
}
|
||||
}
|
||||
|
||||
let speckleType = flat[key.slice(0, -2).concat('speckle_type')]
|
||||
if (!speckleType) return ''
|
||||
|
||||
for (let i = 0; i < speckleTypesWithGeometry.length; i++) {
|
||||
if (speckleType.startsWith(speckleTypesWithGeometry[i])) {
|
||||
return value + ','
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
//called if the received data does not contain objects => it's a table, a list or a single value
|
||||
async function bakeArray(data, context) {
|
||||
export async function bakeArray(data, rowStart, colStart, context) {
|
||||
//it's a single value
|
||||
if (!Array.isArray(data)) {
|
||||
let valueRange = sheet.getCell(rowStart, colStart)
|
||||
@@ -74,33 +123,117 @@ async function bakeArray(data, context) {
|
||||
colIndex++
|
||||
}
|
||||
}
|
||||
//it's a list of lists aka table
|
||||
// it's a list of lists aka table
|
||||
else {
|
||||
let counter = 0
|
||||
let rowIndex = 0
|
||||
for (let array of data) {
|
||||
let colIndex = 0
|
||||
let actualColIndex = 0
|
||||
for (let item of array) {
|
||||
if (headerIndices.length === 0 || headerIndices.includes(colIndex)) {
|
||||
let valueRange = sheet.getCell(rowIndex + rowStart, actualColIndex + colStart)
|
||||
valueRange.values = Array.isArray(item) ? JSON.stringify(item) : item
|
||||
actualColIndex++
|
||||
counter++
|
||||
}
|
||||
colIndex++
|
||||
//sync in batches to avoid a RequestPayloadSizeLimitExceeded
|
||||
if (counter > 5000) {
|
||||
counter = 0
|
||||
await context.sync()
|
||||
}
|
||||
}
|
||||
|
||||
rowIndex++
|
||||
let batchSize = 50
|
||||
while (rowIndex < data.length) {
|
||||
let dataBatch = data.slice(rowIndex, rowIndex + batchSize)
|
||||
let numRows = dataBatch.length
|
||||
let rangeAddress = getRangeAddressFromIndicies(
|
||||
rowStart + rowIndex,
|
||||
colStart,
|
||||
rowStart + rowIndex + numRows - 1,
|
||||
colStart + data[0].length - 1
|
||||
)
|
||||
let valueRange = sheet.getRange(rangeAddress)
|
||||
valueRange.values = dataBatch
|
||||
rowIndex += numRows
|
||||
await context.sync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function bakeTable(data, context, sheet, name, rowStart, colStart, headerRowIndex) {
|
||||
let rowIndex = 0
|
||||
let batchSize = 50
|
||||
while (rowIndex < data.length) {
|
||||
let dataBatch = data.slice(rowIndex, rowIndex + batchSize)
|
||||
let numRows = dataBatch.length
|
||||
let rangeAddress = getRangeAddressFromIndicies(
|
||||
rowStart + headerRowIndex + rowIndex,
|
||||
colStart,
|
||||
rowStart + headerRowIndex + rowIndex + numRows - 1,
|
||||
colStart + data[0].length - 1
|
||||
)
|
||||
let valueRange = sheet.getRange(rangeAddress)
|
||||
valueRange.values = dataBatch
|
||||
rowIndex += numRows
|
||||
await context.sync()
|
||||
}
|
||||
|
||||
let totalRangeAddress = getRangeAddressFromIndicies(
|
||||
rowStart + headerRowIndex,
|
||||
colStart,
|
||||
rowStart + headerRowIndex + data.length - 1,
|
||||
colStart + data[0].length - 1
|
||||
)
|
||||
|
||||
sheet.activate()
|
||||
let table = sheet.tables.add(totalRangeAddress, true)
|
||||
table.load('id')
|
||||
await context.sync()
|
||||
|
||||
let tableId = removeNonAlphanumericCharacters(table.id)
|
||||
let columnMetaAddress = getRangeAddressFromIndicies(
|
||||
rowStart,
|
||||
colStart + 1,
|
||||
rowStart,
|
||||
colStart + data[0].length - 1
|
||||
)
|
||||
let columnMetaRange = sheet.getRange(columnMetaAddress)
|
||||
let rowMetaAddress = getRangeAddressFromIndicies(
|
||||
rowStart,
|
||||
colStart,
|
||||
rowStart + headerRowIndex + rowIndex - 1,
|
||||
colStart
|
||||
)
|
||||
let rowMetaRange = sheet.getRange(rowMetaAddress)
|
||||
|
||||
sheet.names.add(`speckleColumnMetadata_${tableId}`, columnMetaRange)
|
||||
sheet.names.add(`speckleRowMetadata_${tableId}`, rowMetaRange)
|
||||
context.workbook.tables.onDeleted.add(onTableDeleted)
|
||||
context.workbook.tables.onChanged.add(onTableChanged)
|
||||
|
||||
await context.sync()
|
||||
}
|
||||
|
||||
export function removeNonAlphanumericCharacters(s) {
|
||||
return s.replace(/\W/g, '')
|
||||
}
|
||||
|
||||
export function hideRowOrColumn(sheet, columnIndex = -1, rowIndex = -1) {
|
||||
if (columnIndex > -1) {
|
||||
let columnLetter = numberToLetters(columnIndex)
|
||||
sheet.getRange(`${columnLetter}:${columnLetter}`).columnHidden = true
|
||||
}
|
||||
if (rowIndex > -1) {
|
||||
let rowRange = sheet.getRange(`${rowIndex + 1}:${rowIndex + 1}`)
|
||||
rowRange.rowHidden = true
|
||||
rowRange.format.wrapText = true
|
||||
}
|
||||
}
|
||||
|
||||
async function addIdDataToObjectData() {
|
||||
if (
|
||||
arrayData.length != arrayIdData.length ||
|
||||
arrayData.length <= 1 ||
|
||||
!Array.isArray(arrayData[0])
|
||||
) {
|
||||
console.log('Could not attach object ids to table')
|
||||
return
|
||||
}
|
||||
// if all speckleIds are empty strings then don't add the column
|
||||
if (arrayIdData.slice(1, -1).every((val) => !val)) return
|
||||
|
||||
for (let i = 0; i < arrayData.length; i++) {
|
||||
arrayData[i].push(arrayIdData[i])
|
||||
// push an empty space at the end of each array because it will trim the overflow from the
|
||||
// speckleIds in the previous column
|
||||
arrayData[i].push(' ')
|
||||
}
|
||||
}
|
||||
|
||||
function headerListToTree(headers) {
|
||||
let tree = [{ id: 0, name: 'all fields', fullname: '', children: [] }]
|
||||
let i = 1
|
||||
@@ -136,6 +269,114 @@ function hasObjects(data) {
|
||||
return false
|
||||
}
|
||||
|
||||
async function constructRefObjectData(data, nearestObjectId, pathFromNearestObj, signal) {
|
||||
if (!Array.isArray(data)) {
|
||||
if (data.speckle_type == 'reference') {
|
||||
return await getReferencedObject(streamId, data.referencedId, signal)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
if (!nearestObjectId) return data
|
||||
|
||||
const refIndex = data.findIndex((o) => o.speckle_type === 'reference')
|
||||
|
||||
// no referenced Ids, objects are already constructed
|
||||
if (refIndex == -1) return data
|
||||
|
||||
var parent = await getReferencedObject(
|
||||
streamId,
|
||||
nearestObjectId,
|
||||
signal,
|
||||
!pathFromNearestObj.toLowerCase().includes('elements')
|
||||
)
|
||||
if (signal.aborted) return data
|
||||
|
||||
const delimiter = ':::'
|
||||
var pathArray = pathFromNearestObj.split(delimiter)
|
||||
for (let i = 0; i < pathArray.length; i++) {
|
||||
if (!pathArray[i]) continue
|
||||
parent = parent[pathArray[i]]
|
||||
}
|
||||
if (
|
||||
Array.isArray(parent) &&
|
||||
data.length == parent.length &&
|
||||
data[refIndex].referencedId == parent[refIndex].id
|
||||
) {
|
||||
return parent
|
||||
}
|
||||
//TODO: add logging here. If this line is reached then I'm pretty sure I did the traversal logic wrong
|
||||
return data
|
||||
}
|
||||
|
||||
// this function is brought to you by chatGPT
|
||||
// it takes in an excel column index and outputs the column string
|
||||
// 0 -> A
|
||||
// 1 -> B
|
||||
// ...
|
||||
// 26 -> AA
|
||||
// 27 -> AB
|
||||
// ...
|
||||
function numberToLetters(number) {
|
||||
const base = 26
|
||||
let letters = ''
|
||||
do {
|
||||
const remainder = number % base
|
||||
letters = String.fromCharCode(65 + remainder) + letters
|
||||
number = Math.floor(number / base) - 1
|
||||
} while (number >= 0)
|
||||
return letters
|
||||
}
|
||||
|
||||
// this function is also the intellectual property of chatGPT
|
||||
// it does the opposite of the numberToLetters function
|
||||
// A -> 0
|
||||
// B -> 1
|
||||
// ...
|
||||
// AA -> 26
|
||||
// ...
|
||||
function lettersToNumber(letters) {
|
||||
const base = 26
|
||||
let number = 0
|
||||
for (let i = 0; i < letters.length; i++) {
|
||||
const charCode = letters.charCodeAt(i) - 65 + 1
|
||||
number = number * base + charCode
|
||||
}
|
||||
return number - 1
|
||||
}
|
||||
|
||||
export function getRangeAddressFromIndicies(startRow, startCol, endRow, endCol) {
|
||||
let range = ''
|
||||
|
||||
range += numberToLetters(startCol) + String(startRow + 1)
|
||||
range += ':'
|
||||
range += numberToLetters(endCol) + String(endRow + 1)
|
||||
|
||||
return range
|
||||
}
|
||||
|
||||
export function getIndiciesFromRangeAddress(address) {
|
||||
let parts = address.match(/[a-zA-Z]+|[0-9]+/g)
|
||||
|
||||
// if the sheet is part of the address, then get rid of it
|
||||
if (parts[0] === 'Sheet') parts.splice(0, 2)
|
||||
|
||||
let output = []
|
||||
for (let part of parts) {
|
||||
let numValue = parseInt(part)
|
||||
if (numValue) numValue -= 1
|
||||
else numValue = lettersToNumber(part)
|
||||
output.push(numValue)
|
||||
}
|
||||
|
||||
if (output.length == 2) {
|
||||
output[2] = output[0]
|
||||
output[3] = output[1]
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export async function receiveLatest(
|
||||
reference,
|
||||
_streamId,
|
||||
@@ -147,13 +388,21 @@ export async function receiveLatest(
|
||||
try {
|
||||
//TODO: only get objs that are needed?
|
||||
streamId = _streamId
|
||||
let item = await getReferencedObject(reference, signal)
|
||||
let item = await getReferencedObject(streamId, reference, signal)
|
||||
let parts = receiverSelection.fullKeyName.split('.')
|
||||
//picks the sub-object on which the user previously clicked `bake`
|
||||
for (let part of parts) {
|
||||
item = item[part]
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
store.dispatch('showSnackbar', {
|
||||
message: 'Could not match the previous data structure',
|
||||
color: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await bake(
|
||||
item,
|
||||
_streamId,
|
||||
@@ -161,6 +410,8 @@ export async function receiveLatest(
|
||||
_commitMsg,
|
||||
null,
|
||||
signal,
|
||||
null,
|
||||
null,
|
||||
receiverSelection.headers,
|
||||
receiverSelection.range
|
||||
)
|
||||
@@ -173,6 +424,126 @@ export async function receiveLatest(
|
||||
})
|
||||
}
|
||||
}
|
||||
async function getAddress(_streamId, signal, previousRange, context) {
|
||||
let address, range
|
||||
// await window.Excel.run(async (context) => {
|
||||
if (previousRange) {
|
||||
let sheetName = previousRange.split('!')[0].replace(/'/g, '')
|
||||
let rangeAddress = previousRange.split('!')[1]
|
||||
sheet = context.workbook.worksheets.getItem(sheetName)
|
||||
range = sheet.getRange(rangeAddress)
|
||||
} else {
|
||||
range = context.workbook.getSelectedRange()
|
||||
}
|
||||
range.load('address, worksheet, columnIndex, rowIndex')
|
||||
await context.sync()
|
||||
// })
|
||||
|
||||
sheet = range.worksheet
|
||||
sheet.load('items/name')
|
||||
|
||||
address = range.address
|
||||
rowStart = range.rowIndex
|
||||
colStart = range.columnIndex
|
||||
|
||||
streamId = _streamId
|
||||
arrayData = [[]]
|
||||
arrayIdData = ['speckleIDs']
|
||||
|
||||
//if the incoming data has objects we need to flatten them to an array
|
||||
//otherwise we just output it
|
||||
isTabularData = true
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
return address
|
||||
}
|
||||
export async function bakeSchedule(
|
||||
data,
|
||||
_streamId,
|
||||
_commitId,
|
||||
_commitMsg,
|
||||
signal,
|
||||
nearestObjectId,
|
||||
pathFromNearestObj,
|
||||
previousHeaders,
|
||||
previousRange
|
||||
) {
|
||||
try {
|
||||
let selectedHeaders = previousHeaders
|
||||
let address
|
||||
await window.Excel.run(async (context) => {
|
||||
address = await getAddress(_streamId, signal, previousRange, context)
|
||||
data = await constructRefObjectData(data, nearestObjectId, pathFromNearestObj, signal)
|
||||
|
||||
let schedulePaths = []
|
||||
let flat = flatten(data, { maxDepth: 4 })
|
||||
|
||||
for (const [key, value] of Object.entries(flat)) {
|
||||
if (key.endsWith('speckle_type') && value.endsWith('DataTable')) {
|
||||
schedulePaths.push(
|
||||
key.replace('.speckle_type', '').replace('speckle_type', '').split('.')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
context.workbook.worksheets.load('items')
|
||||
await context.sync()
|
||||
|
||||
for (let i = 0; i < schedulePaths.length; i++) {
|
||||
if (i != 0) {
|
||||
context.workbook.worksheets.add()
|
||||
sheet = context.workbook.worksheets.items[-1]
|
||||
}
|
||||
try {
|
||||
let filteredData = { ...data }
|
||||
schedulePaths[i].forEach((step) => {
|
||||
if (step) {
|
||||
filteredData = filteredData[step]
|
||||
}
|
||||
})
|
||||
if (signal.aborted) return
|
||||
|
||||
formatArrayDataForTable(filteredData, arrayData)
|
||||
|
||||
if (signal.aborted) return
|
||||
await bakeDataTable(filteredData, arrayData, context, sheet, rowStart, colStart)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
await context.sync()
|
||||
|
||||
await store.dispatch('receiveCommit', {
|
||||
sourceApplication: 'Excel',
|
||||
streamId: _streamId,
|
||||
commitId: _commitId,
|
||||
message: _commitMsg
|
||||
})
|
||||
|
||||
store.dispatch('showSnackbar', {
|
||||
message: 'Data received successfully'
|
||||
})
|
||||
})
|
||||
|
||||
let receiverSelection = { headers: selectedHeaders, range: address }
|
||||
|
||||
return receiverSelection
|
||||
// eslint-disable-next-line no-unreachable
|
||||
} catch (e) {
|
||||
//pokemon
|
||||
console.log(e)
|
||||
|
||||
let m = 'Something went wrong: ' + e
|
||||
if (e.name !== 'AbortError') m = 'Operation cancelled'
|
||||
|
||||
store.dispatch('showSnackbar', {
|
||||
message: m,
|
||||
color: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
export async function bake(
|
||||
data,
|
||||
_streamId,
|
||||
@@ -180,84 +551,64 @@ export async function bake(
|
||||
_commitMsg,
|
||||
modal,
|
||||
signal,
|
||||
nearestObjectId,
|
||||
pathFromNearestObj,
|
||||
previousHeaders,
|
||||
previousRange
|
||||
) {
|
||||
try {
|
||||
let address, range
|
||||
let selectedHeaders = previousHeaders
|
||||
|
||||
let address
|
||||
await window.Excel.run(async (context) => {
|
||||
if (previousRange) {
|
||||
let sheetName = previousRange.split('!')[0].replace(/'/g, '')
|
||||
let rangeAddress = previousRange.split('!')[1]
|
||||
sheet = context.workbook.worksheets.getItem(sheetName)
|
||||
range = sheet.getRange(rangeAddress)
|
||||
address = await getAddress(_streamId, signal, previousRange, context)
|
||||
data = await constructRefObjectData(data, nearestObjectId, pathFromNearestObj, signal)
|
||||
|
||||
if (signal.aborted) return
|
||||
// check for specific conversions
|
||||
if (checkIfReceivingDataTable(data)) {
|
||||
formatArrayDataForTable(data, arrayData)
|
||||
await bakeDataTable(data, arrayData, context, sheet, rowStart, colStart)
|
||||
} else {
|
||||
range = context.workbook.getSelectedRange()
|
||||
}
|
||||
range.load('address, worksheet, columnIndex, rowIndex')
|
||||
await context.sync()
|
||||
if (hasObjects(data, signal)) {
|
||||
isTabularData = false
|
||||
await flattenData(data, signal)
|
||||
//transpose 2d array, sort alphabetically, then transpose again
|
||||
//this helps ensure the order of the baked columns is the same across streams
|
||||
arrayData = arrayData[0].map((_, colIndex) => arrayData.map((row) => row[colIndex]))
|
||||
arrayData = arrayData.sort((a, b) => (a[0] > b[0] ? 1 : -1))
|
||||
arrayData = arrayData[0].map((_, colIndex) => arrayData.map((row) => row[colIndex]))
|
||||
} else arrayData = data
|
||||
|
||||
sheet = range.worksheet
|
||||
sheet.load('items/name')
|
||||
if (signal.aborted) return
|
||||
|
||||
address = range.address
|
||||
rowStart = range.rowIndex
|
||||
colStart = range.columnIndex
|
||||
|
||||
streamId = _streamId
|
||||
arrayData = [[]]
|
||||
headerIndices = []
|
||||
|
||||
//if the incoming data has objects we need to flatten them to an array
|
||||
//otherwise we just output it
|
||||
isTabularData = true
|
||||
|
||||
if (signal.aborted) return
|
||||
if (hasObjects(data, signal)) {
|
||||
isTabularData = false
|
||||
await flattenData(data, signal)
|
||||
//transpose 2d array, sort alphabetically, then transpose again
|
||||
//this helps ensure the order of the baked columns is the same across streams
|
||||
arrayData = arrayData[0].map((_, colIndex) => arrayData.map((row) => row[colIndex]))
|
||||
arrayData = arrayData.sort((a, b) => (a[0] > b[0] ? 1 : -1))
|
||||
arrayData = arrayData[0].map((_, colIndex) => arrayData.map((row) => row[colIndex]))
|
||||
} else arrayData = data
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
if (!isTabularData && arrayData[0].length > 25) {
|
||||
//it's manual run
|
||||
if (!previousHeaders && modal) {
|
||||
let headers = headerListToTree(arrayData[0], signal)
|
||||
let dialog = await modal.open(
|
||||
headers,
|
||||
`You are about to receive ${arrayData[0].length} columns and ${arrayData.length} rows, you can filter them below.`
|
||||
)
|
||||
console.log(dialog)
|
||||
if (!dialog.result) {
|
||||
store.dispatch('showSnackbar', {
|
||||
message: 'Operation cancelled'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (arrayData[0].length !== dialog.items.length) {
|
||||
selectedHeaders = dialog.items
|
||||
//construct a list of the index of each header to include
|
||||
for (let item of selectedHeaders) headerIndices.push(arrayData[0].indexOf(item))
|
||||
}
|
||||
} else if (previousHeaders) {
|
||||
for (let item of previousHeaders) {
|
||||
let index = arrayData[0].indexOf(item)
|
||||
if (index !== -1) headerIndices.push(index)
|
||||
if (!isTabularData && arrayData[0].length > 25) {
|
||||
if (!previousHeaders && modal) {
|
||||
let headers = headerListToTree(arrayData[0], signal)
|
||||
let dialog = await modal.open(
|
||||
headers,
|
||||
`You are about to receive ${arrayData[0].length} columns and ${arrayData.length} rows, you can filter them below.`
|
||||
)
|
||||
console.log(dialog)
|
||||
if (!dialog.result) {
|
||||
store.dispatch('showSnackbar', {
|
||||
message: 'Operation cancelled'
|
||||
})
|
||||
return
|
||||
}
|
||||
selectedHeaders = filterArrayData(dialog.items, arrayData)
|
||||
} else if (previousHeaders) {
|
||||
selectedHeaders = filterArrayData(previousHeaders, arrayData)
|
||||
}
|
||||
|
||||
console.log(arrayData)
|
||||
}
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
await addIdDataToObjectData()
|
||||
await bakeArray(arrayData, rowStart, colStart, context)
|
||||
}
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
await bakeArray(arrayData, context)
|
||||
await context.sync()
|
||||
|
||||
await store.dispatch('receiveCommit', {
|
||||
@@ -290,6 +641,27 @@ export async function bake(
|
||||
}
|
||||
}
|
||||
|
||||
function filterArrayData(headers, allData) {
|
||||
if (allData[0].length == headers.length) return allData
|
||||
|
||||
let filteredData = [[]]
|
||||
// initialize filteredData array with empty arrays
|
||||
for (let i = 0; i < allData.length; i++) {
|
||||
filteredData[i] = []
|
||||
}
|
||||
|
||||
for (let item of headers) {
|
||||
let index = allData[0].indexOf(item)
|
||||
if (index === -1) continue
|
||||
|
||||
for (let i = 0; i < allData.length; i++) {
|
||||
filteredData[i].push(allData[i][index])
|
||||
}
|
||||
}
|
||||
return filteredData
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export async function send(savedStream, streamId, branchName, message) {
|
||||
try {
|
||||
await window.Excel.run(async (context) => {
|
||||
@@ -303,26 +675,36 @@ export async function send(savedStream, streamId, branchName, message) {
|
||||
let values = range.values
|
||||
|
||||
let data = []
|
||||
if (savedStream.hasHeaders) {
|
||||
for (let row = 1; row < values.length; row++) {
|
||||
let object = {}
|
||||
for (let col = 0; col < values[0].length; col++) {
|
||||
let propName = values[0][col]
|
||||
//if (propName !== 'id' && propName.endsWith('.id')) continue
|
||||
let propValue = values[row][col]
|
||||
object[propName] = propValue
|
||||
}
|
||||
let unlattened = unflatten(object)
|
||||
data.push(unlattened)
|
||||
}
|
||||
// check for specific conversion
|
||||
let table = await getDataTableContainingRange(range, sheet, context)
|
||||
if (table) {
|
||||
data = await BuildDataTableObject(range, values, table, sheet, context)
|
||||
} else {
|
||||
for (let row = 0; row < values.length; row++) {
|
||||
let rowArray = []
|
||||
for (let col = 0; col < values[0].length; col++) {
|
||||
rowArray.push(values[row][col])
|
||||
if (savedStream.hasHeaders) {
|
||||
for (let row = 1; row < values.length; row++) {
|
||||
let object = {}
|
||||
for (let col = 0; col < values[0].length; col++) {
|
||||
let propName = values[0][col]
|
||||
//if (propName !== 'id' && propName.endsWith('.id')) continue
|
||||
let propValue = values[row][col]
|
||||
object[propName] = propValue
|
||||
}
|
||||
// generate a hash if none is present
|
||||
object.id = object.id || MD5(JSON.stringify(object)).toString(enc.Hex)
|
||||
let unlattened = unflatten(object)
|
||||
data.push(unlattened)
|
||||
}
|
||||
} else {
|
||||
for (let row = 0; row < values.length; row++) {
|
||||
let rowArray = []
|
||||
for (let col = 0; col < values[0].length; col++) {
|
||||
rowArray.push(values[row][col])
|
||||
}
|
||||
data.push(rowArray)
|
||||
}
|
||||
data.push(rowArray)
|
||||
}
|
||||
|
||||
data = { data: data, speckle_type: 'Base' }
|
||||
}
|
||||
|
||||
await store.dispatch('createCommit', {
|
||||
|
||||
+14
-5
@@ -11,11 +11,6 @@ const routes = [
|
||||
name: 'streams',
|
||||
component: () => import('../views/Streams.vue')
|
||||
},
|
||||
{
|
||||
path: '/add',
|
||||
name: 'add',
|
||||
component: () => import('../views/Add.vue')
|
||||
},
|
||||
{
|
||||
path: '/streams/:streamId/commits/:commitId',
|
||||
name: 'commit',
|
||||
@@ -24,11 +19,25 @@ const routes = [
|
||||
},
|
||||
component: () => import('../views/Commit.vue')
|
||||
},
|
||||
{
|
||||
path: '/streams/:streamId/:commitId?',
|
||||
name: 'stream',
|
||||
meta: {
|
||||
title: 'Stream | Speckle'
|
||||
},
|
||||
component: () => import('../views/SingleStream.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('../views/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/singleStream',
|
||||
name: 'singleStream',
|
||||
component: () => import('../views/SingleStream.vue')
|
||||
},
|
||||
{
|
||||
path: '/redirect',
|
||||
name: 'redirect'
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ export default new Vuex.Store({
|
||||
variables: {
|
||||
object: {
|
||||
streamId: streamId,
|
||||
objects: [{ data: object, speckle_type: 'Base' }]
|
||||
objects: [object]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,7 +4,8 @@ export default {
|
||||
},
|
||||
mutations: {
|
||||
ADD_STREAM(state, value) {
|
||||
state.streams.unshift(value)
|
||||
const index = state.streams.findIndex((x) => x.id === value.id)
|
||||
if (index == -1) state.streams.unshift(value)
|
||||
},
|
||||
REMOVE_STREAM(state, value) {
|
||||
const index = state.streams.findIndex((x) => x.id === value)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
require('url')
|
||||
|
||||
export class StreamWrapper {
|
||||
constructor(streamIdOrUrl, accountId, serverUrl) {
|
||||
this.originalOutput = streamIdOrUrl
|
||||
try {
|
||||
this.streamWrapperFromUrl(streamIdOrUrl)
|
||||
} catch (e) {
|
||||
this.serverUrl = serverUrl
|
||||
this.userId = accountId
|
||||
this.streamId = streamIdOrUrl
|
||||
}
|
||||
}
|
||||
|
||||
streamWrapperFromUrl(streamUrl) {
|
||||
this.url = new URL(streamUrl)
|
||||
this.segments = this.url.pathname.split('/').map((segment) => segment + '/')
|
||||
this.serverUrl = this.url.origin
|
||||
|
||||
if (this.segments.length >= 4 && this.segments[3]?.toLowerCase() === 'branches/') {
|
||||
this.streamId = this.segments[2].replace('/', '')
|
||||
if (this.segments.length > 5) {
|
||||
let branchSegments = this.segments.slice(4, this.segments.length - 1)
|
||||
this.branchName = branchSegments.join('')
|
||||
} else {
|
||||
this.branchName = this.segments[4]
|
||||
}
|
||||
} else {
|
||||
switch (this.segments.length) {
|
||||
case 3: // ie http://speckle.server/streams/8fecc9aa6d
|
||||
if (this.segments[1].toLowerCase() === 'streams/')
|
||||
this.streamId = this.segments[2].replace('/', '')
|
||||
else throw new Error(`Cannot parse ${this.originalOutput} into a stream wrapper class`)
|
||||
break
|
||||
case 4: // ie https://speckle.server/streams/0c6ad366c4/globals/
|
||||
if (this.segments[3].toLowerCase().startsWith('globals')) {
|
||||
this.streamId = this.segments[2].replace('/', '')
|
||||
this.branchName = this.segments[3].replace('/', '')
|
||||
} else throw new Error(`Cannot parse ${this.originalOutput} into a stream wrapper class`)
|
||||
break
|
||||
case 5: // ie http://speckle.server/streams/8fecc9aa6d/commits/76a23d7179
|
||||
switch (this.segments[3].toLowerCase()) {
|
||||
case 'commits/':
|
||||
this.streamId = this.segments[2].replace('/', '')
|
||||
this.commitId = this.segments[4].replace('/', '')
|
||||
break
|
||||
case 'globals/':
|
||||
this.streamId = this.segments[2].replace('/', '')
|
||||
this.branchName = this.segments[3].replace('/', '')
|
||||
this.commitId = this.segments[4].replace('/', '')
|
||||
break
|
||||
case 'branches/':
|
||||
this.streamId = this.segments[2].replace('/', '')
|
||||
this.branchName = this.segments[4].replace('/', '')
|
||||
break
|
||||
case 'objects/':
|
||||
this.streamId = this.segments[2].replace('/', '')
|
||||
this.objectId = this.segments[4].replace('/', '')
|
||||
break
|
||||
default:
|
||||
throw new Error(`Cannot parse ${this.originalOutput} into a stream wrapper class`)
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Cannot parse ${this.originalOutput} into a stream wrapper class`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row align="center">
|
||||
<v-col cols="12" align="center" class="mt-5">
|
||||
<p v-if="filteredStreams && filteredStreams.length > 0" class="subtitle">
|
||||
Click on a stream to add it to this document. 👇
|
||||
</p>
|
||||
<p v-else-if="search" class="subtitle">No streams found 🧐</p>
|
||||
<div v-else-if="!$apollo.loading">
|
||||
<p class="subtitle">
|
||||
You don't have any streams 😟,
|
||||
<a :href="serverUrl" target="_blank">visit Speckle web to create one</a>
|
||||
!
|
||||
</p>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row align="center" class="my-0 py-0">
|
||||
<v-col cols="12" class="my-0 py-0" align="center">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
rounded
|
||||
filled
|
||||
clearable
|
||||
label="Search"
|
||||
class="mx-5 search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="mt-0 pt-0">
|
||||
<v-col cols="12" class="mt-0 pt-0">
|
||||
<v-card elevation="0" color="transparent">
|
||||
<div v-if="$apollo.loading" class="mx-4">
|
||||
<v-skeleton-loader class="mt-3" type="article"></v-skeleton-loader>
|
||||
<v-skeleton-loader class="mt-3" type="article"></v-skeleton-loader>
|
||||
<v-skeleton-loader class="mt-3" type="article"></v-skeleton-loader>
|
||||
</div>
|
||||
<v-card-text v-if="streams" class="mt-0 pt-3">
|
||||
<div v-for="(stream, i) in filteredStreams" :key="i">
|
||||
<list-item-stream :stream="stream"></list-item-stream>
|
||||
</div>
|
||||
<infinite-loading
|
||||
v-if="streams && streams.items && streams.items.length < streams.totalCount"
|
||||
@infinite="infiniteHandler"
|
||||
>
|
||||
<div slot="no-more">These are all your streams!</div>
|
||||
<div slot="no-results">There are no streams to load</div>
|
||||
</infinite-loading>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListItemStream from '../components/ListItemStream'
|
||||
import gql from 'graphql-tag'
|
||||
import streamsQuery from '../graphql/streams.gql'
|
||||
import InfiniteLoading from 'vue-infinite-loading'
|
||||
import { createClient } from '../vue-apollo'
|
||||
|
||||
export default {
|
||||
name: 'Add',
|
||||
|
||||
components: {
|
||||
ListItemStream,
|
||||
InfiniteLoading
|
||||
},
|
||||
data: () => ({
|
||||
streams: [],
|
||||
search: ''
|
||||
}),
|
||||
apollo: {
|
||||
$client: createClient(),
|
||||
streams: {
|
||||
prefetch: true,
|
||||
query: streamsQuery,
|
||||
fetchPolicy: 'cache-and-network', //https://www.apollographql.com/docs/react/data/queries/
|
||||
variables() {
|
||||
return {
|
||||
query: this.search
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return this.search && this.search.length > 0 && this.search.length < 3
|
||||
},
|
||||
debounce: 300
|
||||
},
|
||||
$subscribe: {
|
||||
userStreamAdded: {
|
||||
query: gql`
|
||||
subscription {
|
||||
userStreamAdded
|
||||
}
|
||||
`,
|
||||
result() {
|
||||
this.$apollo.queries.streams.refetch()
|
||||
},
|
||||
skip() {
|
||||
return !this.user
|
||||
}
|
||||
},
|
||||
userStreamRemoved: {
|
||||
query: gql`
|
||||
subscription {
|
||||
userStreamRemoved
|
||||
}
|
||||
`,
|
||||
result() {
|
||||
this.$apollo.queries.streams.refetch()
|
||||
}
|
||||
// skip() {
|
||||
// return !this.isAuthenticated
|
||||
// }
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isAuthenticated() {
|
||||
return this.$store.getters.isAuthenticated
|
||||
},
|
||||
serverUrl() {
|
||||
return this.$store.getters.serverUrl
|
||||
},
|
||||
filteredStreams() {
|
||||
if (!this.streams.items) return null
|
||||
return this.streams.items.filter(
|
||||
(x) => this.$store.state.streams.streams.findIndex((y) => y.id === x.id) === -1
|
||||
)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$mixpanel.track('Connector Action', { name: 'Stream List' })
|
||||
},
|
||||
methods: {
|
||||
infiniteHandler($state) {
|
||||
this.$apollo.queries.streams.fetchMore({
|
||||
variables: {
|
||||
cursor: this.streams.cursor
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
const newItems = fetchMoreResult.streams.items
|
||||
|
||||
//set vue-infinite state
|
||||
if (newItems.length === 0) $state.complete()
|
||||
else $state.loaded()
|
||||
|
||||
return {
|
||||
streams: {
|
||||
__typename: previousResult.streams.__typename,
|
||||
totalCount: fetchMoreResult.streams.totalCount,
|
||||
cursor: fetchMoreResult.streams.cursor,
|
||||
// Merging the new streams
|
||||
items: [...previousResult.streams.items, ...newItems]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.search .v-text-field__details {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col class="py-2">
|
||||
<v-btn text large color="primary" to="/">
|
||||
<v-btn text large color="primary" :to="`/streams/${streamId}`">
|
||||
<v-icon dark>mdi-chevron-left</v-icon>
|
||||
back
|
||||
</v-btn>
|
||||
@@ -47,7 +47,7 @@
|
||||
<object-speckle-viewer
|
||||
v-if="stream"
|
||||
:stream-id="stream.id"
|
||||
:object-id="stream.commit.referencedObject"
|
||||
:nearest-object-id="stream.commit.referencedObject"
|
||||
:value="commitObject"
|
||||
:downloadable="false"
|
||||
:expand="true"
|
||||
@@ -90,6 +90,9 @@ export default {
|
||||
speckle_type: 'reference',
|
||||
referencedId: this.stream.commit.referencedObject
|
||||
}
|
||||
},
|
||||
streamId() {
|
||||
return this.$route.params.streamId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
<template>
|
||||
<v-card v-if="error" class="pa-5 mb-3" style="transition: all 0.2s">
|
||||
<v-card-title class="subtitle-1 px-0 pt-0">
|
||||
{{ error }} ⚠️
|
||||
<div class="floating">
|
||||
<!-- <v-btn
|
||||
v-tooltip="`Remove this stream from the document`"
|
||||
small
|
||||
icon
|
||||
color="red"
|
||||
@click="remove"
|
||||
>
|
||||
<v-icon small>mdi-minus-circle-outline</v-icon>
|
||||
</v-btn> -->
|
||||
<v-btn
|
||||
v-tooltip="`Open this stream in a new window`"
|
||||
small
|
||||
icon
|
||||
color="primary"
|
||||
:href="`${serverUrl}/streams/${savedStream.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon small>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-text class="px-0">
|
||||
<span v-if="error == 'Stream not found'">
|
||||
The stream might have been deleted or belog to another Speckle server
|
||||
</span>
|
||||
<span v-if="error == 'You do not have access to this resource.'">
|
||||
Please ask the stream owner for access or to make it public
|
||||
</span>
|
||||
<br />
|
||||
Stream Id: {{ savedStream.id }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div v-else-if="$apollo.queries.stream.loading" class="mx-0 mb-3 fill-height background-light">
|
||||
<div class="progress-parent">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
:size="150"
|
||||
class="fill-height"
|
||||
></v-progress-circular>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="stream" id="viewer-parent" class="background-light">
|
||||
<div v-if="viewerLoading" class="progress-parent">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
:size="150"
|
||||
class="fill-height"
|
||||
></v-progress-circular>
|
||||
</div>
|
||||
<div id="viewer"></div>
|
||||
<div id="stream-info-parent">
|
||||
<StreamController
|
||||
ref="streamController"
|
||||
:stream="stream"
|
||||
:determined-conversion="determinedConversion"
|
||||
@loadByReferencedId="loadViewerObjectByReferencedId"
|
||||
@loadByCommitId="loadViewerObjectByCommitId"
|
||||
@clearDeterminedConversion="clearDeterminedConversion"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import StreamController from '../components/StreamController.vue'
|
||||
import streamQuery from '../graphql/stream.gql'
|
||||
import {
|
||||
send,
|
||||
receiveLatest,
|
||||
getIndiciesFromRangeAddress,
|
||||
getRangeAddressFromIndicies
|
||||
} from '../plugins/excel'
|
||||
import gql from 'graphql-tag'
|
||||
import { createClient } from '../vue-apollo'
|
||||
import { Viewer, ViewerEvent } from '@speckle/viewer'
|
||||
// import router from '../router'
|
||||
|
||||
let ac = new AbortController()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StreamController
|
||||
},
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
// remove on selection changed event that is tied to the viewer
|
||||
if (this.onSelectionChangedEvent) {
|
||||
await window.Excel.run(this.onSelectionChangedEvent.context, async (context) => {
|
||||
this.onSelectionChangedEvent.remove()
|
||||
await context.sync()
|
||||
this.onSelectionChangedEvent = null
|
||||
})
|
||||
}
|
||||
next()
|
||||
},
|
||||
props: {
|
||||
streamId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
commitId: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
progress: false,
|
||||
message: '',
|
||||
viewer: null,
|
||||
objectIds: null,
|
||||
selectedObjectIds: null,
|
||||
filterViewer: true,
|
||||
isReceiver: true,
|
||||
selection: null,
|
||||
hasHeaders: false,
|
||||
viewerLoading: false,
|
||||
referencedObject: null,
|
||||
onSelectionChangedEvent: null,
|
||||
determinedConversion: ''
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
stream: {
|
||||
prefetch: true,
|
||||
query: streamQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
variables() {
|
||||
return {
|
||||
id: this.streamId
|
||||
}
|
||||
},
|
||||
result() {
|
||||
const index = this.$store.state.streams.streams.findIndex((x) => x.id === this.streamId)
|
||||
let savedStream = null
|
||||
if (index > -1) {
|
||||
savedStream = this.$store.state.streams.streams[index]
|
||||
this.$nextTick(function () {
|
||||
this.$refs.streamController.isReceiver = savedStream.isReceiver
|
||||
this.$refs.streamController.selection = savedStream.selection
|
||||
this.$refs.streamController.hasHeaders = savedStream.hasHeaders
|
||||
this.$refs.streamController.selectedBranchName = savedStream.selectedBranchName
|
||||
this.$refs.streamController.selectedCommitId = savedStream.selectedCommitId
|
||||
this.$refs.streamController.receiverSelection = savedStream.receiverSelection
|
||||
})
|
||||
} else {
|
||||
this.$nextTick(function () {
|
||||
this.$refs.streamController.isReceiver = true
|
||||
this.$refs.streamController.selectedBranchName = this.$refs.streamController.selectedBranch.name
|
||||
this.$refs.streamController.selectedCommitId = this.selectedCommit.id
|
||||
})
|
||||
}
|
||||
|
||||
// if this page is reached via a link with a commit id, this set the branch and id on the card
|
||||
if (this.commitId) {
|
||||
this.$nextTick(function () {
|
||||
this.$refs.streamController.selectedCommitId = this.commitId
|
||||
const branch = this.stream.branches.items.find((x) =>
|
||||
x.commits.items.findIndex(
|
||||
(y) => y.id == this.$refs.streamController.selectedCommitId - 1
|
||||
)
|
||||
)
|
||||
this.$refs.streamController.selectedBranchName = branch.name
|
||||
})
|
||||
}
|
||||
},
|
||||
error(error) {
|
||||
console.log(this.error)
|
||||
this.error = JSON.stringify(error.message)
|
||||
.replaceAll('"', '')
|
||||
.replace('GraphQL error: ', '')
|
||||
console.log(this.error)
|
||||
}
|
||||
},
|
||||
$client: createClient(),
|
||||
$subscribe: {
|
||||
streamUpdated: {
|
||||
query: gql`
|
||||
subscription($id: String!) {
|
||||
streamUpdated(streamId: $id)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.streamId }
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
},
|
||||
commitCreated: {
|
||||
query: gql`
|
||||
subscription($streamId: String!) {
|
||||
commitCreated(streamId: $streamId)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { streamId: this.streamId }
|
||||
},
|
||||
result(commitInfo) {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
if (this.isReceiver)
|
||||
this.$store.dispatch('showSnackbar', {
|
||||
message: `New commit on ${this.stream.name} @ ${commitInfo.data.commitCreated.branchName}`
|
||||
})
|
||||
}
|
||||
},
|
||||
commitUpdated: {
|
||||
query: gql`
|
||||
subscription($id: String!) {
|
||||
commitUpdated(streamId: $id)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.streamId }
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
},
|
||||
branchCreated: {
|
||||
query: gql`
|
||||
subscription($id: String!) {
|
||||
branchCreated(streamId: $id)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.streamId }
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
},
|
||||
branchDeleted: {
|
||||
query: gql`
|
||||
subscription($id: String!) {
|
||||
branchDeleted(streamId: $id)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.streamId }
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
},
|
||||
branchUpdated: {
|
||||
query: gql`
|
||||
subscription($id: String!) {
|
||||
branchUpdated(streamId: $id)
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { id: this.streamId }
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
serverUrl() {
|
||||
return this.$store.getters.serverUrl
|
||||
},
|
||||
savedStream() {
|
||||
return {
|
||||
id: this.streamId,
|
||||
isReceiver: this.isReceiver,
|
||||
selection: this.selection,
|
||||
hasHeaders: this.hasHeaders,
|
||||
selectedBranchName: this.$refs.streamController.selectedBranchName,
|
||||
selectedCommitId: this.$refs.streamController.selectedCommitId,
|
||||
receiverSelection: this.receiverSelection
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkForDeterminedConversions() {
|
||||
const dataTree = this.viewer.getDataTree()
|
||||
// Get all mesh speckle objects
|
||||
try {
|
||||
const schedule = dataTree.findFirst((guid, obj) => {
|
||||
return obj.speckle_type.endsWith('DataTable')
|
||||
})
|
||||
|
||||
if (schedule) {
|
||||
this.determinedConversion = 'Schedule'
|
||||
} else {
|
||||
this.determinedConversion = ''
|
||||
}
|
||||
} catch {
|
||||
this.determinedConversion = ''
|
||||
}
|
||||
},
|
||||
clearDeterminedConversion() {
|
||||
this.determinedConversion = ''
|
||||
},
|
||||
async initViewer() {
|
||||
if (this.viewer) {
|
||||
return
|
||||
}
|
||||
|
||||
var container = document.getElementById('viewer')
|
||||
var v = new Viewer(container)
|
||||
await v.init()
|
||||
|
||||
// highlight selected objects in sheet
|
||||
v.on(ViewerEvent.ObjectClicked, async (data) => {
|
||||
console.log(data?.hits[0]?.object.id)
|
||||
var speckleId = data?.hits[0]?.object.id
|
||||
if (speckleId == undefined) v.resetFilters()
|
||||
else {
|
||||
v.selectObjects(new Array(data?.hits[0]?.object.id))
|
||||
await window.Excel.run(async (context) => {
|
||||
var sheet = context.workbook.worksheets.getActiveWorksheet()
|
||||
var range = sheet.getUsedRange()
|
||||
var found = range.findOrNullObject(speckleId, {
|
||||
completeMatch: false, // Match the whole cell value.
|
||||
matchCase: false, // Don't match case.
|
||||
searchDirection: window.Excel.SearchDirection.forward // Start search at the beginning of the range.
|
||||
})
|
||||
|
||||
found.load('address')
|
||||
// Update the fill color
|
||||
if (found) {
|
||||
// var extendedRange = found.get
|
||||
this.filterViewer = false
|
||||
found.getExtendedRange(window.Excel.KeyboardDirection.left, found).select()
|
||||
}
|
||||
await context.sync()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// highlight selected objects in viewer
|
||||
await window.Excel.run(async (context) => {
|
||||
var sheet = context.workbook.worksheets.getActiveWorksheet()
|
||||
this.onSelectionChangedEvent ??= sheet.onSelectionChanged.add(this.checkModelForSelection)
|
||||
await context.sync()
|
||||
})
|
||||
|
||||
v.setLightConfiguration({
|
||||
enabled: true,
|
||||
castShadow: false, // there is a bug involving the shadows so turn them off for now
|
||||
intensity: 5,
|
||||
color: 0xffffff,
|
||||
elevation: 1.33,
|
||||
azimuth: 0.75,
|
||||
radius: 0,
|
||||
indirectLightIntensity: 1.2,
|
||||
shadowcatcher: true
|
||||
})
|
||||
this.viewer = v
|
||||
},
|
||||
async loadViewerObjectByReferencedId(referencedObject) {
|
||||
if (referencedObject === this.referencedObject) return
|
||||
if (this.viewerLoading) {
|
||||
await this.viewer?.cancelLoad(
|
||||
`${this.serverUrl}/streams/${this.streamId}/objects/${this.referencedObject}`,
|
||||
true
|
||||
)
|
||||
this.viewerLoading = false
|
||||
}
|
||||
this.referencedObject = referencedObject
|
||||
await this.initViewer()
|
||||
await this.viewer?.unloadAll()
|
||||
this.viewerLoading = true
|
||||
|
||||
const APP_NAME = process.env.VUE_APP_SPECKLE_NAME
|
||||
const TOKEN = `${APP_NAME}.AuthToken`
|
||||
try {
|
||||
await this.viewer?.loadObject(
|
||||
`${this.serverUrl}/streams/${this.streamId}/objects/${referencedObject}`,
|
||||
localStorage.getItem(TOKEN)
|
||||
)
|
||||
} finally {
|
||||
if (referencedObject == this.referencedObject) this.viewerLoading = false
|
||||
}
|
||||
this.checkForDeterminedConversions()
|
||||
},
|
||||
async loadViewerObjectByCommitId(commitId) {
|
||||
const index = this.$refs.streamController.selectedBranch.commits.items.findIndex(
|
||||
(x) => x.id === commitId
|
||||
)
|
||||
|
||||
await this.loadViewerObjectByReferencedId(
|
||||
this.$refs.streamController.selectedBranch.commits.items[index].referencedObject
|
||||
)
|
||||
},
|
||||
async checkModelForSelection() {
|
||||
if (!this.filterViewer) {
|
||||
this.filterViewer = true
|
||||
return
|
||||
}
|
||||
let speckleIdColIndex = await this.getSpeckleIdsColIndex()
|
||||
await window.Excel.run(async (context) => {
|
||||
// Get the selected range.
|
||||
let range = context.workbook.getSelectedRange()
|
||||
range.load('address')
|
||||
await context.sync()
|
||||
let selectedRangeIndicies = getIndiciesFromRangeAddress(range.address)
|
||||
|
||||
if (selectedRangeIndicies[0] > speckleIdColIndex) {
|
||||
this.unisolateObjects()
|
||||
return
|
||||
}
|
||||
|
||||
let speckleIdRangeAddress = getRangeAddressFromIndicies(
|
||||
selectedRangeIndicies[1],
|
||||
speckleIdColIndex,
|
||||
selectedRangeIndicies[3],
|
||||
speckleIdColIndex
|
||||
)
|
||||
let speckleIdRange = context.workbook.worksheets
|
||||
.getActiveWorksheet()
|
||||
.getRange(speckleIdRangeAddress)
|
||||
speckleIdRange.load('text')
|
||||
await context.sync()
|
||||
|
||||
let idsInViewer = new Array()
|
||||
for (let i = 0; i < speckleIdRange.text?.length; i++) {
|
||||
for (let j = 0; j < speckleIdRange.text[i].length; j++) {
|
||||
if (speckleIdRange.text[i][j].length < 32) continue
|
||||
let splitIDs = speckleIdRange.text[i][j].split(',')
|
||||
for (let id = 0; id < splitIDs.length; id++) {
|
||||
if (splitIDs[id].length == 32) idsInViewer.push(splitIDs[id])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.unisolateObjects()
|
||||
|
||||
if (idsInViewer.length > 0) {
|
||||
this.viewer?.isolateObjects(idsInViewer)
|
||||
this.selectedObjectIds = idsInViewer
|
||||
// this.viewer?.zoom(idsInViewer)
|
||||
}
|
||||
})
|
||||
},
|
||||
unisolateObjects() {
|
||||
// unisolate previous objects
|
||||
if (this.selectedObjectIds?.length > 0) {
|
||||
this.viewer?.resetFilters()
|
||||
this.viewer?.unIsolateObjects(this.selectedObjectIds)
|
||||
this.selectedObjectIds = null
|
||||
}
|
||||
},
|
||||
async getSpeckleIdsColIndex() {
|
||||
return await window.Excel.run(async (context) => {
|
||||
let sheet = context.workbook.worksheets.getActiveWorksheet()
|
||||
let usedRange = sheet.getUsedRange()
|
||||
|
||||
// TODO: we may need to narrow the search field for large wbs
|
||||
// or if we want to have more stable support for multiple tables in the same sheet
|
||||
|
||||
var found = usedRange.findOrNullObject('speckleIDs', {
|
||||
completeMatch: true, // Match the whole cell value.
|
||||
matchCase: true, // Don't match case.
|
||||
searchDirection: window.Excel.SearchDirection.forward // Start search at the beginning of the range.
|
||||
})
|
||||
found.load('address')
|
||||
await context.sync()
|
||||
|
||||
var idHeaderAddressIndicies = getIndiciesFromRangeAddress(found.address)
|
||||
return idHeaderAddressIndicies[0]
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
ac.abort()
|
||||
},
|
||||
async send() {
|
||||
// these values need to be set to null or the models will not load
|
||||
// when switching back to the receive mode
|
||||
this.viewer = null
|
||||
this.referencedObject = null
|
||||
this.$store.dispatch('addStream', this.savedStream)
|
||||
|
||||
this.$mixpanel.track('Send')
|
||||
send(
|
||||
this.savedStream,
|
||||
this.stream.id,
|
||||
this.$refs.streamController.selectedBranch.name,
|
||||
this.message
|
||||
)
|
||||
},
|
||||
async receiveLatest() {
|
||||
// this.$mixpanel.track('Receive')
|
||||
ac = new AbortController()
|
||||
|
||||
console.log(this.savedStream.receiverSelection)
|
||||
|
||||
this.progress = true
|
||||
await receiveLatest(
|
||||
this.selectedCommit.referencedObject,
|
||||
this.stream.id,
|
||||
this.selectedCommit.id,
|
||||
this.selectedCommit.message,
|
||||
this.savedStream.receiverSelection,
|
||||
ac.signal
|
||||
)
|
||||
this.progress = false
|
||||
},
|
||||
formatCommitName(id) {
|
||||
if (this.$refs.streamController.selectedBranch.commits.items[0].id == id) {
|
||||
return 'latest'
|
||||
}
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.stream-card-select .v-text-field__details {
|
||||
display: none !important;
|
||||
}
|
||||
.v-btn .lower {
|
||||
text-transform: none;
|
||||
}
|
||||
.floating {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 10px;
|
||||
}
|
||||
#stream-info-parent {
|
||||
/* max-width: 600px; */
|
||||
/* left: 50%;
|
||||
transform: translateX(-50%); */
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
bottom: 0px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
#stream-info {
|
||||
max-width: 400px;
|
||||
margin: 15px;
|
||||
padding: 0 !important;
|
||||
}
|
||||
#stream-info .row {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
#viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#viewer-parent {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.background-light {
|
||||
background: #8e9eab;
|
||||
background: -webkit-linear-gradient(to top right, #eeeeee, #c8e8ff) !important;
|
||||
background: linear-gradient(to top right, #ffffff, #c8e8ff) !important;
|
||||
}
|
||||
|
||||
.background-dark {
|
||||
background: #141e30;
|
||||
background: -webkit-linear-gradient(to top left, #243b55, #141e30) !important;
|
||||
background: linear-gradient(to top left, #243b55, #141e30) !important;
|
||||
}
|
||||
|
||||
.progress-parent {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.v-progress-circular {
|
||||
height: 100%;
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
+161
-21
@@ -2,25 +2,52 @@
|
||||
<v-container>
|
||||
<v-row align="center">
|
||||
<v-col cols="12" align="center" class="mt-5">
|
||||
<span v-if="streams && streams.length > 0" class="subtitle">
|
||||
You have {{ streams.length }} stream{{ streams.length === 1 ? '' : 's' }}
|
||||
in this document 🙌
|
||||
</span>
|
||||
|
||||
<div v-else>
|
||||
<p class="subtitle">You have no streams in this document</p>
|
||||
<v-btn large class="mt-5" color="primary" to="add">Add a stream</v-btn>
|
||||
<p v-if="search" class="subtitle">No streams found 🧐</p>
|
||||
<div v-else-if="!$apollo.loading && filteredStreams && filteredStreams.length == 0">
|
||||
<p class="subtitle">
|
||||
You don't have any streams 😟,
|
||||
<a :href="serverUrl" target="_blank">visit Speckle web to create one</a>
|
||||
!
|
||||
</p>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-row align="center" class="my-0 py-0">
|
||||
<v-col cols="12" class="my-0 py-0" align="center">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
rounded
|
||||
filled
|
||||
clearable
|
||||
label="Search"
|
||||
class="mx-5 search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<create-stream-dialog :account-id="user.id" :server-url="serverUrl" />
|
||||
|
||||
<v-row class="mt-0 pt-0">
|
||||
<v-col cols="12" class="mt-0 pt-0">
|
||||
<v-card elevation="0" color="transparent">
|
||||
<v-card-text class="mt-0 pt-3">
|
||||
<div v-for="stream in streams" :key="stream.id">
|
||||
<stream-card :saved-stream="stream"></stream-card>
|
||||
<div v-if="$apollo.loading" class="mx-4">
|
||||
<v-skeleton-loader class="mt-3" type="article"></v-skeleton-loader>
|
||||
<v-skeleton-loader class="mt-3" type="article"></v-skeleton-loader>
|
||||
<v-skeleton-loader class="mt-3" type="article"></v-skeleton-loader>
|
||||
</div>
|
||||
<v-card-text v-if="streams" class="mt-0 pt-3">
|
||||
<div v-for="(stream, i) in filteredStreams" :key="i">
|
||||
<list-item-stream :stream="stream"></list-item-stream>
|
||||
</div>
|
||||
<infinite-loading
|
||||
v-if="streams && streams.items && streams.items.length < streams.totalCount"
|
||||
@infinite="infiniteHandler"
|
||||
>
|
||||
<div slot="no-more">These are all your streams!</div>
|
||||
<div slot="no-results">There are no streams to load</div>
|
||||
</infinite-loading>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@@ -29,20 +56,133 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StreamCard from '../components/StreamCard'
|
||||
import ListItemStream from '../components/ListItemStream'
|
||||
import gql from 'graphql-tag'
|
||||
import streamsQuery from '../graphql/streams.gql'
|
||||
import InfiniteLoading from 'vue-infinite-loading'
|
||||
import { createClient } from '../vue-apollo'
|
||||
import CreateStreamDialog from '../components/dialogs/CreateStreamDialog.vue'
|
||||
|
||||
export default {
|
||||
name: 'Streams',
|
||||
name: 'Add',
|
||||
|
||||
components: {
|
||||
StreamCard
|
||||
ListItemStream,
|
||||
InfiniteLoading,
|
||||
CreateStreamDialog
|
||||
},
|
||||
computed: {
|
||||
streams() {
|
||||
if (!this.$store.state) return []
|
||||
return this.$store.state.streams.streams
|
||||
data: () => ({
|
||||
streams: [],
|
||||
search: ''
|
||||
}),
|
||||
apollo: {
|
||||
$client: createClient(),
|
||||
streams: {
|
||||
prefetch: true,
|
||||
query: streamsQuery,
|
||||
fetchPolicy: 'cache-and-network', //https://www.apollographql.com/docs/react/data/queries/
|
||||
variables() {
|
||||
return {
|
||||
query: this.search
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return this.search && this.search.length > 0 && this.search.length < 3
|
||||
},
|
||||
debounce: 300
|
||||
},
|
||||
$subscribe: {
|
||||
userStreamAdded: {
|
||||
query: gql`
|
||||
subscription {
|
||||
userStreamAdded
|
||||
}
|
||||
`,
|
||||
result() {
|
||||
this.$apollo.queries.streams.refetch()
|
||||
},
|
||||
skip() {
|
||||
return !this.user
|
||||
}
|
||||
},
|
||||
userStreamRemoved: {
|
||||
query: gql`
|
||||
subscription {
|
||||
userStreamRemoved
|
||||
}
|
||||
`,
|
||||
result() {
|
||||
this.$apollo.queries.streams.refetch()
|
||||
}
|
||||
// skip() {
|
||||
// return !this.isAuthenticated
|
||||
// }
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
computed: {
|
||||
isAuthenticated() {
|
||||
console.log(this.user)
|
||||
return this.$store.getters.isAuthenticated
|
||||
},
|
||||
user() {
|
||||
return this.$store.state.user.user
|
||||
},
|
||||
serverUrl() {
|
||||
return this.$store.getters.serverUrl
|
||||
},
|
||||
filteredStreams() {
|
||||
console.log('user', this.$store.state.user.user)
|
||||
if (!this.streams.items) return null
|
||||
|
||||
let savedStreams = this.streams.items.filter(
|
||||
(x) => this.$store.state.streams.streams.findIndex((y) => y.id === x.id) !== -1
|
||||
)
|
||||
let otherStreams = this.streams.items.filter(
|
||||
(x) => this.$store.state.streams.streams.findIndex((y) => y.id === x.id) === -1
|
||||
)
|
||||
|
||||
savedStreams.push(...otherStreams)
|
||||
return savedStreams
|
||||
// return this.streams.items.filter(
|
||||
// (x) => this.$store.state.streams.streams.findIndex((y) => y.id === x.id) === -1
|
||||
// )
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$mixpanel.track('Connector Action', { name: 'Stream List' })
|
||||
},
|
||||
methods: {
|
||||
infiniteHandler($state) {
|
||||
this.$apollo.queries.streams.fetchMore({
|
||||
variables: {
|
||||
cursor: this.streams.cursor
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
const newItems = fetchMoreResult.streams.items
|
||||
|
||||
//set vue-infinite state
|
||||
if (newItems.length === 0) $state.complete()
|
||||
else $state.loaded()
|
||||
|
||||
return {
|
||||
streams: {
|
||||
__typename: previousResult.streams.__typename,
|
||||
totalCount: fetchMoreResult.streams.totalCount,
|
||||
cursor: fetchMoreResult.streams.cursor,
|
||||
// Merging the new streams
|
||||
items: [...previousResult.streams.items, ...newItems]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.search .v-text-field__details {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
+4
-1
@@ -10,5 +10,8 @@ module.exports = {
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
},
|
||||
transpileDependencies: ['vuetify', '@speckle/objectloader', 'flatted', 'vuex-persist']
|
||||
transpileDependencies: ['vuetify', '@speckle/objectloader', 'flatted', 'vuex-persist'],
|
||||
configureWebpack: (config) => {
|
||||
config.devtool = 'source-map'
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user