79 Commits

Author SHA1 Message Date
Iain Sproat f85c029765 chore(domains): speckle.xyz is replaced by app.speckle.systems (#64) 2024-07-18 16:55:19 +01:00
Matteo Cominetti 8aeb882e9e fix: reduces number of collaborators to show 2023-08-01 12:46:51 +01:00
Matteo Cominetti f01e0d3e89 fix: removes unused user query 2023-08-01 12:46:24 +01:00
Matteo Cominetti 47170c0241 ditto 2023-08-01 11:59:27 +01:00
Matteo Cominetti 4c898679ad fix: replace user query with otherUser 2023-08-01 11:59:14 +01:00
Matteo Cominetti 809ea7bb8d fix: replace materialdesign cdn with npm fix #52 2023-08-01 11:58:48 +01:00
Matteo Cominetti 4995ecf28e Merge pull request #51 from specklesystems/connor/all-fields-receive-fix
fix : receiving all fields
2023-07-04 18:27:58 +01:00
Connor Ivy 99cc44f518 fix receiving all fields 2023-07-03 14:50:58 -05:00
Matteo Cominetti 925ece5fa6 Merge pull request #49 from specklesystems/connor/fix-entire-table-selected
fix entire table selected
2023-06-30 08:13:32 +01:00
Matteo Cominetti b547b9e237 Merge pull request #50 from specklesystems/connor/viewer-upgrade
chore: upgrade viewer
2023-06-30 08:13:07 +01:00
Connor Ivy 208d937698 upgrade viewer 2023-06-29 11:53:26 -05:00
Connor Ivy 488deca574 fix entire table selected 2023-06-28 15:46:54 -05:00
Matteo Cominetti cf9112c714 Merge pull request #45 from specklesystems/connor/minor-fixes
minor fixes
2023-05-19 16:59:14 +01:00
Connor Ivy f11378e1d2 don't add speckleIds if empty 2023-05-19 07:56:53 -05:00
Connor Ivy 95be883b16 minor fixes 2023-05-19 07:02:45 -05:00
Matteo Cominetti 985064287b Merge pull request #44 from specklesystems/connor/fix-table-heading-selection
fix sending when headers are selected
2023-05-12 17:45:56 +01:00
Connor Ivy f5f99ba3b2 fix sending when headers are selected 2023-05-12 11:41:43 -05:00
Matteo Cominetti 22fd03125a Merge pull request #43 from specklesystems/connor/excel-fiddle
Connor/excel fiddle
2023-05-12 16:13:05 +01:00
Connor Ivy ce193b41ad fix: turn top left cell into value deserializable as Base 2023-05-08 12:05:14 -05:00
Connor Ivy e3af320f4d added updating base on table appId 2023-05-05 13:29:47 -05:00
Connor Ivy dfb5fd444a create new sheets for subsequent tables 2023-05-05 11:38:30 -05:00
Connor Ivy ffdb9cd2b5 deleting table cleans up metadata 2023-05-05 11:16:45 -05:00
Connor Ivy 872c2552c0 fixes to recieve datatables from excel 2023-05-04 15:45:10 -05:00
Connor Ivy 56f9f2747a upgrade viewer package 2023-05-04 15:42:49 -05:00
Connor Ivy 5a407d45f0 query viewer for schedules instead of server 2023-05-04 15:08:43 -05:00
Connor Ivy 89e6706ee4 reformat 2023-05-04 12:09:13 -05:00
Connor Ivy 1dee2e50fb Merge remote-tracking branch 'origin/connor/excel-fiddle' into connor/excel-fiddle 2023-05-04 12:05:12 -05:00
Connor Ivy 9fba4d6207 better ux for receiving schedules 2023-05-04 12:05:01 -05:00
Matteo Cominetti f1968ac990 chore: local debug 2023-05-03 20:47:47 +01:00
Connor Ivy 5425bfae33 hide rows before baking 2023-04-11 16:37:14 -05:00
Connor Ivy c1c48eeb1e fix table extension method to find metaRow 2023-04-11 16:34:23 -05:00
Connor Ivy 2dcb21bbd6 grey out readonly params 2023-04-10 09:15:45 -05:00
Connor Ivy 0dc2d8abc6 add support for receiving schedules list 2023-04-07 17:21:25 -05:00
Connor Ivy f55d410911 set applicationId on send 2023-04-06 12:33:10 -05:00
Connor Ivy 1a5837e28a sending table out as datatable 2023-04-06 12:00:47 -05:00
Connor Ivy 79d3d15e9e add hidden metadata for columns 2023-04-05 15:00:18 -05:00
Connor Ivy cfe0078fc6 correctly hiding metadate rows and cols 2023-04-05 14:19:47 -05:00
Connor Ivy b9cacf8440 get ready for sending 2023-04-04 17:25:29 -05:00
Connor Ivy 7002f75c6d Merge remote-tracking branch 'origin/main' into connor/excel-fiddle 2023-04-04 07:56:52 -05:00
Connor Ivy 6d3fd718ba receiving DataTables 2023-04-04 07:56:46 -05:00
Matteo Cominetti 3a361a9e16 Delete .github/workflows directory 2023-02-15 12:45:39 +00:00
Matteo Cominetti f751c45cf6 Merge pull request #41 from specklesystems/connor/excel-fiddle
Feat: Add viewer and much faster receiving / baking
2023-02-15 12:42:21 +00:00
Connor Ivy 9c7e5f5276 check if selectionEvent needs to be removed 2023-02-13 14:33:31 -06:00
Connor Ivy e135471d4a route to correct commit when opening a stream by URL 2023-02-12 14:37:44 -06:00
Connor Ivy e345c2aef8 more code cleanup 2023-02-12 13:44:42 -06:00
Connor Ivy a2b523aadc delete unused code 2023-02-12 13:33:04 -06:00
Connor Ivy 17e484301c fix receive previous selection 2023-02-12 13:28:05 -06:00
Connor Ivy 7f73317bc4 more stable way of mapping selected cells to viewer components 2023-02-12 12:49:31 -06:00
Connor Ivy ebc7707b01 let the user selection the speckle id if they want 2023-02-10 16:06:13 -06:00
Connor Ivy 11dcaa6bfd fix nested list receive logic 2023-02-10 15:44:26 -06:00
Connor Ivy 5e057b2ed0 unregister selection event when leaving page 2023-02-10 15:10:17 -06:00
Connor Ivy 288e113d57 another significant performance boost 2023-02-10 15:09:34 -06:00
Connor Ivy 6f2d4b0dd5 changed viewer id tracking method 2023-02-10 12:22:04 -06:00
Connor Ivy bdb78f2d2f add missing prop 2023-02-09 16:29:25 -06:00
Connor Ivy 5b902578fc private streams and receiveLastSelection 2023-02-09 16:23:49 -06:00
Connor Ivy 4579ead966 waaaaaay better loading times 2023-02-09 16:08:45 -06:00
Connor Ivy 9dc13940eb rewire the views 2023-02-08 13:32:40 -06:00
Connor Ivy 6ec73b1aad fix model loading after send bug 2023-02-07 17:21:24 -06:00
Connor Ivy 0830c4c818 working receiving and sending 2023-02-07 16:42:44 -06:00
Connor Ivy 71d76a239a better formatting 2023-01-27 10:39:28 -06:00
Connor Ivy 3170255986 receiving performance improvement 2023-01-27 10:38:38 -06:00
Connor Ivy dc25e59ada style everything! 2023-01-25 16:26:51 -06:00
Connor Ivy 5d82e2ab5f filtering looks pretty decent 2023-01-25 15:51:29 -06:00
Connor Ivy f8c91ec062 able to add streams by url and id 2023-01-25 15:16:57 -06:00
Connor Ivy 17d0e93ec7 cleanup unused code 2023-01-25 11:11:31 -06:00
Connor Ivy b3bc596e84 trying to make it look good 2023-01-25 07:38:02 -06:00
Connor Ivy d0fbb350d3 this works but most likely has a bunch of garbage in it 2023-01-24 15:28:32 -06:00
Connor Ivy f8c1d9f96e list items take you to singleStream view 2023-01-24 15:26:56 -06:00
Connor Ivy 5cb0a1eafd dev env improvements 2023-01-24 15:25:03 -06:00
Connor Ivy ceccc31ec7 add cool speckle background to viewer 2023-01-24 15:24:46 -06:00
Connor Ivy 1d0798081d gettings the ids as part of the table now works 2023-01-23 19:07:31 -06:00
Connor Ivy 5b40aab84c can lookup values in viewer 2023-01-22 01:53:14 -06:00
Connor Ivy 4e8a6015cb missing with binding to events 2023-01-21 20:08:14 -06:00
Connor Ivy 8b9d950376 the viewer looks better but still not great 2023-01-21 12:31:52 -06:00
Connor Ivy 9eef750890 mostly working debug config 2023-01-21 09:40:44 -06:00
Connor Ivy 92dd0583f9 viewer loads an object! 2023-01-20 22:09:22 -06:00
Connor Ivy 2cb1207999 don't need this code 2023-01-19 17:00:41 -06:00
Connor Ivy 1289a2a842 this works for debugging 2023-01-19 16:32:56 -06:00
Matteo Cominetti a44fa41bbf feat: hash and create object id if missing, update object loader library 2023-01-10 16:02:47 +00:00
34 changed files with 3581 additions and 1143 deletions
-78
View File
@@ -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 }}
-50
View File
@@ -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
-1
View File
@@ -15,7 +15,6 @@ pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
+23
View File
@@ -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",
}
}
]
}
+1 -1
View File
@@ -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.
+915 -118
View File
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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>
-5
View File
@@ -110,11 +110,6 @@ export default {
drawer: null,
showSnackbar: false,
items: [
{
name: 'Add stream',
icon: '',
to: '/add'
},
{
name: 'Streams',
icon: '📃',
+20 -15
View File
@@ -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}`)
}
}
}
+19 -2
View File
@@ -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
+8
View File
@@ -77,6 +77,14 @@ export default {
commitMsg: {
type: String,
default: null
},
nearestObjectId: {
type: String,
default: null
},
pathFromNearestObject: {
type: String,
default: null
}
},
data() {
+32 -15
View File
@@ -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
+4
View File
@@ -32,6 +32,10 @@ export default {
commitMsg: {
type: String,
default: null
},
nearestObjectId: {
type: String,
default: null
}
}
}
View File
-521
View File
@@ -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>
+436
View File
@@ -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>
+10 -5
View File
@@ -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>
+2 -3
View File
@@ -1,7 +1,6 @@
query User($id: String!) {
user(id: $id) {
query LimitedUser($id: String!) {
otherUser(id: $id) {
id
email
name
bio
company
+1
View File
@@ -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
+463
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -209,7 +209,7 @@ export default new Vuex.Store({
variables: {
object: {
streamId: streamId,
objects: [{ data: object, speckle_type: 'Base' }]
objects: [object]
}
}
})
+2 -1
View File
@@ -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)
+69
View File
@@ -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`)
}
}
}
}
-172
View File
@@ -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>
+5 -2
View File
@@ -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
}
}
}
+590
View File
@@ -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
View File
@@ -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
View File
@@ -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'
}
}