Merge remote-tracking branch 'origin/main' into alan/admin-panel
This commit is contained in:
+13
-5
@@ -33,7 +33,7 @@ workflows:
|
||||
context: main-builds
|
||||
filters:
|
||||
branches:
|
||||
only: cristi/ci-test
|
||||
only: cristi/ci-k8s-tor
|
||||
|
||||
jobs:
|
||||
test_server:
|
||||
@@ -90,13 +90,21 @@ jobs:
|
||||
command: env SPECKLE_SERVER_PACKAGE=preview-service ./.circleci/build.sh
|
||||
- run:
|
||||
name: Deploy
|
||||
command: ./.circleci/deploy.sh
|
||||
command: |
|
||||
./.circleci/deploy.sh
|
||||
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
|
||||
env K8S_CLUSTER=TOR1 K8S_NAMESPACE=${K8S_NAMESPACE_TOR1_1_RELEASE} ./.circleci/deploy_in_new_setup.sh
|
||||
else
|
||||
env K8S_CLUSTER=TOR1 K8S_NAMESPACE=${K8S_NAMESPACE_TOR1_1_LATEST} ./.circleci/deploy_in_new_setup.sh
|
||||
fi
|
||||
- run:
|
||||
name: Test deployment
|
||||
command: |
|
||||
./utils/test-deployment/install_prerequisites.sh
|
||||
SPECKLE_SERVER=https://latest.speckle.dev
|
||||
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
|
||||
SPECKLE_SERVER=https://speckle.xyz
|
||||
./utils/test-deployment/run_tests.py https://speckle.xyz
|
||||
./utils/test-deployment/run_tests.py ${SPECKLE_URL_TOR1_1_RELEASE}
|
||||
else
|
||||
./utils/test-deployment/run_tests.py https://latest.speckle.dev
|
||||
./utils/test-deployment/run_tests.py ${SPECKLE_URL_TOR1_1_LATEST}
|
||||
fi
|
||||
./utils/test-deployment/run_tests.py $SPECKLE_SERVER
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
K8S_CLUSTER_CERTIFICATE_VARIABLE=K8S_${K8S_CLUSTER}_CERTIFICATE
|
||||
K8S_CLUSTER_CERTIFICATE=${!K8S_CLUSTER_CERTIFICATE_VARIABLE}
|
||||
|
||||
K8S_TOKEN_VARIABLE=K8S_${K8S_CLUSTER}_TOKEN
|
||||
K8S_TOKEN=${!K8S_TOKEN_VARIABLE}
|
||||
|
||||
K8S_SERVER_VARIABLE=K8S_${K8S_CLUSTER}_SERVER
|
||||
K8S_SERVER=${!K8S_SERVER_VARIABLE}
|
||||
|
||||
# K8S_NAMESPACE
|
||||
|
||||
IMAGE_VERSION_TAG=$CIRCLE_SHA1
|
||||
|
||||
if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then
|
||||
IMAGE_VERSION_TAG=$CIRCLE_TAG
|
||||
fi
|
||||
|
||||
echo "$K8S_CLUSTER_CERTIFICATE" | base64 --decode > k8s_cert.crt
|
||||
|
||||
# Update deployments
|
||||
./kubectl \
|
||||
--kubeconfig=/dev/null \
|
||||
--server=$K8S_SERVER \
|
||||
--certificate-authority=k8s_cert.crt \
|
||||
--token=$K8S_TOKEN \
|
||||
--namespace=$K8S_NAMESPACE \
|
||||
set image deployment/speckle-frontend main=$DOCKER_IMAGE_TAG-frontend:$IMAGE_VERSION_TAG
|
||||
|
||||
./kubectl \
|
||||
--kubeconfig=/dev/null \
|
||||
--server=$K8S_SERVER \
|
||||
--certificate-authority=k8s_cert.crt \
|
||||
--token=$K8S_TOKEN \
|
||||
--namespace=$K8S_NAMESPACE \
|
||||
set image deployment/speckle-server main=$DOCKER_IMAGE_TAG-server:$IMAGE_VERSION_TAG
|
||||
|
||||
./kubectl \
|
||||
--kubeconfig=/dev/null \
|
||||
--server=$K8S_SERVER \
|
||||
--certificate-authority=k8s_cert.crt \
|
||||
--token=$K8S_TOKEN \
|
||||
--namespace=$K8S_NAMESPACE \
|
||||
set image deployment/speckle-preview-service main=$DOCKER_IMAGE_TAG-preview-service:$IMAGE_VERSION_TAG
|
||||
|
||||
|
||||
# Wait for rollout to complete
|
||||
./kubectl \
|
||||
--kubeconfig=/dev/null \
|
||||
--server=$K8S_SERVER \
|
||||
--certificate-authority=k8s_cert.crt \
|
||||
--token=$K8S_TOKEN \
|
||||
--namespace=$K8S_NAMESPACE \
|
||||
rollout status -w deployment/speckle-frontend --timeout=3m
|
||||
|
||||
./kubectl \
|
||||
--kubeconfig=/dev/null \
|
||||
--server=$K8S_SERVER \
|
||||
--certificate-authority=k8s_cert.crt \
|
||||
--token=$K8S_TOKEN \
|
||||
--namespace=$K8S_NAMESPACE \
|
||||
rollout status -w deployment/speckle-server --timeout=3m
|
||||
|
||||
./kubectl \
|
||||
--kubeconfig=/dev/null \
|
||||
--server=$K8S_SERVER \
|
||||
--certificate-authority=k8s_cert.crt \
|
||||
--token=$K8S_TOKEN \
|
||||
--namespace=$K8S_NAMESPACE \
|
||||
rollout status -w deployment/speckle-preview-service --timeout=3m
|
||||
Generated
+13
@@ -12963,6 +12963,11 @@
|
||||
"is-plain-obj": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"sortablejs": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
|
||||
"integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
|
||||
},
|
||||
"source-list-map": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
||||
@@ -14821,6 +14826,14 @@
|
||||
"@seregpie/claw": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"vuedraggable": {
|
||||
"version": "2.24.3",
|
||||
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",
|
||||
"integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
|
||||
"requires": {
|
||||
"sortablejs": "1.10.2"
|
||||
}
|
||||
},
|
||||
"vuetify": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.4.0.tgz",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"vue-matomo": "^3.14.0-0",
|
||||
"vue-router": "^3.4.9",
|
||||
"vue-timeago": "^5.1.2",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuetify": "^2.3.21",
|
||||
"vuetify-image-input": "^19.1.0",
|
||||
"vuex": "^3.6.0"
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<v-card rounded="lg" class="pa-3 mb-3" elevation="0">
|
||||
<v-dialog v-model="saveDialog" max-width="500">
|
||||
<globals-save-dialog
|
||||
:branch-name="branchName"
|
||||
:stream-id="$route.params.streamId"
|
||||
:commit-obj="globalsCommit"
|
||||
@close="closeSaveDialog"
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-card-title>Globals</v-card-title>
|
||||
<v-card-subtitle v-if="commitMessage">
|
||||
<v-icon dense class="text-subtitle-1">mdi-source-commit</v-icon>
|
||||
{{ commitMessage }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
These global variables can be used for storing design values, project requirements, notes, or
|
||||
any info you want to keep track of alongside your geometry. Variable values can be text, numbers,
|
||||
lists, or booleans. Click the box icon next to any field to turn it into a nested group of
|
||||
fields, and drag and drop fields in and out of groups as you please! Note that field order
|
||||
may not always be preserved.
|
||||
</v-card-text>
|
||||
<v-card-text v-if="!(userRole === 'contributor') && !(userRole === 'owner')">
|
||||
You are free to play around with the globals here, but you do not have the required stream
|
||||
permission to save your changes.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-switch
|
||||
v-model="deleteEntries"
|
||||
v-tooltip="'Toggle delete mode'"
|
||||
class="ml-3"
|
||||
dense
|
||||
inset
|
||||
color="error"
|
||||
:label="`DELETE`"
|
||||
></v-switch>
|
||||
<v-spacer />
|
||||
<v-btn v-tooltip="'Clear all globals'" color="primary" small @click="clearGlobals">
|
||||
clear
|
||||
</v-btn>
|
||||
<v-btn v-tooltip="'Undo any changes'" color="primary" small @click="resetGlobals">
|
||||
reset all
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="userRole === 'contributor' || userRole === 'owner'"
|
||||
v-tooltip="'Save your changes with a message'"
|
||||
small
|
||||
:disabled="!globalsAreValid"
|
||||
color="primary"
|
||||
@click="
|
||||
saveDialog = true
|
||||
deleteEntries = false
|
||||
"
|
||||
>
|
||||
save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
<v-card-text>
|
||||
<globals-entry
|
||||
v-if="!$apollo.loading"
|
||||
:entries="globalsArray"
|
||||
:path="[]"
|
||||
:remove="deleteEntries"
|
||||
@add-prop="addProp"
|
||||
@remove-prop="removeProp"
|
||||
@field-to-object="fieldToObject"
|
||||
@object-to-field="objectToField"
|
||||
/>
|
||||
<div v-else>
|
||||
<v-skeleton-loader type="list-item-three-line" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import crs from 'crypto-random-string'
|
||||
import objectQuery from '../graphql/objectSingle.gql'
|
||||
|
||||
export default {
|
||||
name: 'GlobalsBuilder',
|
||||
components: {
|
||||
GlobalsEntry: () => import('../components/GlobalsEntry'),
|
||||
GlobalsSaveDialog: () => import('../components/dialogs/GlobalsSaveDialog')
|
||||
},
|
||||
apollo: {
|
||||
object: {
|
||||
query: objectQuery,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.streamId,
|
||||
id: this.objectId
|
||||
}
|
||||
},
|
||||
update(data) {
|
||||
delete data.stream.object.data.__closure
|
||||
this.globalsArray = this.nestedGlobals(data.stream.object.data)
|
||||
return data.stream.object
|
||||
},
|
||||
skip() {
|
||||
return this.objectId == null
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
userRole: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
branchName: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
objectId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
streamId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
commitMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
globalsArray: [],
|
||||
globalsAreValid: true,
|
||||
saveDialog: false,
|
||||
deleteEntries: false,
|
||||
sample: {
|
||||
Region: 'London',
|
||||
'Project Code': 'GB123456',
|
||||
'Linked Projects': ['GB654321', 'EU424242'],
|
||||
'Project Lead': 'Sir Spockle II',
|
||||
'Pretty Cool?': true,
|
||||
Climate: {
|
||||
'Summer DBT [C]': 35,
|
||||
'Summer WBT [C]': 20,
|
||||
'Winter DBT [C]': -4,
|
||||
'Winter WBT [C]': -4,
|
||||
Enthalpy: {
|
||||
'Summer Enthalpy [kJ/kg]': 56.87,
|
||||
'Winter Enthalpy [kJ/kg]': 2.74
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
globalsCommit() {
|
||||
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||
this.globalsAreValid = true
|
||||
let base = this.globalsToBase(this.globalsArray)
|
||||
return base
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.objectId) {
|
||||
this.globalsArray = this.nestedGlobals(this.sample)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
nestedGlobals(data) {
|
||||
if (!data) return []
|
||||
let entries = Object.entries(data)
|
||||
let arr = []
|
||||
for (let [key, val] of entries) {
|
||||
if (key.startsWith('__')) continue
|
||||
if (['totalChildrenCount', 'speckle_type', 'id'].includes(key)) continue
|
||||
|
||||
if (!Array.isArray(val) && typeof val === 'object' && val !== null) {
|
||||
if (val.speckle_type && val.speckle_type === 'reference') {
|
||||
arr.push({
|
||||
key,
|
||||
valid: true,
|
||||
id: crs({ length: 10 }),
|
||||
value: val,
|
||||
globals: this.nestedGlobals(val),
|
||||
type: 'object' //TODO: handle references
|
||||
})
|
||||
} else {
|
||||
arr.push({
|
||||
key,
|
||||
valid: true,
|
||||
id: crs({ length: 10 }),
|
||||
value: val,
|
||||
globals: this.nestedGlobals(val),
|
||||
type: 'object'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
arr.push({
|
||||
key,
|
||||
valid: true,
|
||||
id: crs({ length: 10 }),
|
||||
value: val,
|
||||
type: 'field'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return arr
|
||||
},
|
||||
globalsToBase(arr) {
|
||||
let base = {
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Base',
|
||||
id: null
|
||||
}
|
||||
|
||||
for (let entry of arr) {
|
||||
if (!entry.value && !entry.globals) return
|
||||
|
||||
if (!entry.valid) this.globalsAreValid = false
|
||||
|
||||
if (Array.isArray(entry.value)) base[entry.key] = entry.value
|
||||
else if (entry.type == 'object') {
|
||||
base[entry.key] = this.globalsToBase(entry.globals)
|
||||
} else if (typeof entry.value === 'string' && entry.value.includes(',')) {
|
||||
base[entry.key] = entry.value
|
||||
.replace(/\s/g, '')
|
||||
.split(',')
|
||||
.map((el) => (isNaN(el) ? el : parseFloat(el)))
|
||||
} else if (typeof entry.value === 'boolean') {
|
||||
base[entry.key] = entry.value
|
||||
} else {
|
||||
base[entry.key] = isNaN(entry.value) ? entry.value : parseFloat(entry.value)
|
||||
}
|
||||
}
|
||||
|
||||
return base
|
||||
},
|
||||
resetGlobals() {
|
||||
this.deleteEntries = false
|
||||
this.globalsArray = this.object?.data
|
||||
? this.nestedGlobals(this.object.data)
|
||||
: this.nestedGlobals(this.sample)
|
||||
},
|
||||
clearGlobals() {
|
||||
this.globalsArray = this.nestedGlobals({ placeholder: 'something cool goes here...' })
|
||||
},
|
||||
addProp(kwargs) {
|
||||
let globals = this.getNestedGlobals(kwargs.path)
|
||||
globals.splice(globals.length, 0, kwargs.field)
|
||||
},
|
||||
removeProp(kwargs) {
|
||||
let globals = this.getNestedGlobals(kwargs.path)
|
||||
globals.splice(kwargs.index, 1)
|
||||
},
|
||||
fieldToObject(kwargs) {
|
||||
let globals = this.getNestedGlobals(kwargs.path)
|
||||
|
||||
globals.splice(kwargs.index, 1, kwargs.obj)
|
||||
},
|
||||
objectToField(kwargs) {
|
||||
let globals = this.getNestedGlobals(kwargs.path)
|
||||
|
||||
globals.splice(kwargs.index, 1, ...kwargs.fields)
|
||||
},
|
||||
getNestedGlobals(path) {
|
||||
let entry = this.globalsArray
|
||||
if (!path) return entry
|
||||
|
||||
let depth = path.length
|
||||
|
||||
if (depth > 0) {
|
||||
let id = path.shift()
|
||||
entry = entry.find((e) => e.id == id)
|
||||
}
|
||||
|
||||
if (depth > 1) {
|
||||
path.forEach((id) => {
|
||||
entry = entry.globals.find((e) => e.id == id)
|
||||
})
|
||||
}
|
||||
|
||||
if (!Array.isArray(entry)) entry = entry.globals
|
||||
|
||||
return entry
|
||||
},
|
||||
closeSaveDialog() {
|
||||
this.saveDialog = false
|
||||
this.$emit('new-commit')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<draggable
|
||||
:list="entries"
|
||||
class="dragArea pl-0"
|
||||
tag="ul"
|
||||
group="globals"
|
||||
v-bind="dragOptions"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
>
|
||||
<div v-for="(entry, index) in entries" :key="entry.id">
|
||||
<transition type="transition" :name="!drag ? 'flip-list' : null">
|
||||
<div v-if="!entry.globals">
|
||||
<div class="d-flex align-center">
|
||||
<v-btn
|
||||
v-if="remove"
|
||||
class="entry-delete mr-5"
|
||||
fab
|
||||
rounded
|
||||
x-small
|
||||
color="error"
|
||||
@click="emitRemoveAt(index)"
|
||||
>
|
||||
<v-icon>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
<v-text-field
|
||||
ref="keyInput"
|
||||
v-model="entry.key"
|
||||
:rules="rules.keys(index, entries)"
|
||||
:error-messages="
|
||||
entry.valid
|
||||
? null
|
||||
: entry.key
|
||||
? 'Each property name must be unique'
|
||||
: 'This property needs a name!'
|
||||
"
|
||||
class="entry-key mr-5"
|
||||
hint="property name"
|
||||
filled
|
||||
dense
|
||||
rounded
|
||||
/>
|
||||
<v-text-field v-model="entry.value" class="entry-value mr-5" hint="property value" />
|
||||
<v-btn
|
||||
v-if="!remove"
|
||||
v-tooltip="'Transform this field into an object'"
|
||||
icon
|
||||
small
|
||||
@click="emitFieldToObject(entry, index)"
|
||||
>
|
||||
<v-icon color="primary">mdi-cube-outline</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<v-card v-else rounded="lg" class="pa-3 my-6" elevation="4">
|
||||
<v-row align="center">
|
||||
<v-col>
|
||||
<v-card-title
|
||||
v-if="!editTitle"
|
||||
@mouseenter="mouseOver = true"
|
||||
@mouseleave="mouseOver = false"
|
||||
>
|
||||
<v-btn
|
||||
v-if="remove"
|
||||
class="entry-delete mr-5"
|
||||
fab
|
||||
rounded
|
||||
x-small
|
||||
color="error"
|
||||
@click="emitRemoveAt(index)"
|
||||
>
|
||||
<v-icon>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
{{ entry.key }}
|
||||
<v-btn v-if="mouseOver" icon small color="primary" @click="editTitle = true">
|
||||
<v-icon small>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-title v-else>
|
||||
<v-text-field
|
||||
ref="keyInput"
|
||||
v-model="entry.key"
|
||||
:rules="rules.keys(index, entries)"
|
||||
:error-messages="
|
||||
entry.valid
|
||||
? null
|
||||
: entry.key
|
||||
? 'Each property name must be unique'
|
||||
: 'This property needs a name!'
|
||||
"
|
||||
></v-text-field>
|
||||
<v-btn icon color="primary" @click="editTitle = false">
|
||||
<v-icon small>mdi-check</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
v-tooltip="'Flatten this object into fields'"
|
||||
class="mr-3"
|
||||
icon
|
||||
small
|
||||
@click="emitObjectToField(entry, index)"
|
||||
>
|
||||
<v-icon color="primary">mdi-arrow-collapse-down</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<globals-entry
|
||||
:entries="entry.globals"
|
||||
:path="[...path, entry.id]"
|
||||
:remove="remove"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</v-card>
|
||||
</transition>
|
||||
</div>
|
||||
</draggable>
|
||||
<div
|
||||
v-if="!remove"
|
||||
slot="footer"
|
||||
key="footer"
|
||||
class="btn-group list-group-item mt-3"
|
||||
role="group"
|
||||
aria-label="Basic example"
|
||||
>
|
||||
<v-btn
|
||||
v-tooltip="'Add a new field to this object'"
|
||||
color="primary"
|
||||
rounded
|
||||
fab
|
||||
small
|
||||
@click="emitAddProp"
|
||||
>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import draggable from 'vuedraggable'
|
||||
import crs from 'crypto-random-string'
|
||||
|
||||
export default {
|
||||
name: 'GlobalsEntry',
|
||||
components: { draggable },
|
||||
props: {
|
||||
entries: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
path: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
streamId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
remove: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
invalidIds: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editTitle: false,
|
||||
mouseOver: false,
|
||||
drag: false,
|
||||
valid: true,
|
||||
rules: {
|
||||
keys(index, entries) {
|
||||
return [
|
||||
(v) => {
|
||||
let result = !!v || 'Properties need to have a name!'
|
||||
entries[index].valid = result === true
|
||||
return result
|
||||
},
|
||||
(v) => {
|
||||
let filtered = entries.filter((_, i) => i != index)
|
||||
let result =
|
||||
filtered.findIndex((e) => e.key === v) === -1 || 'Each property name must be unique'
|
||||
entries[index].valid = !!v && result === true
|
||||
return result
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
errors: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dragOptions() {
|
||||
return {
|
||||
animation: 150,
|
||||
disabled: false,
|
||||
ghostClass: 'ghost'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emitAddProp() {
|
||||
var bimNouns = ['parameter', 'BIM', 'triple O', 'digital twin', 'LOD 9000', 'automation', 'structure', 'layer', 'interop']
|
||||
var bimAdjs = ['parametric', 'chonky', '3D', 'liminal', 'brutalist', 'postmodern', 'discrete', 'dank']
|
||||
var bimExclamations = ['wow', 'much', 'yes', 'towards a new']
|
||||
var randomPhrase =
|
||||
bimExclamations[Math.floor(Math.random() * bimExclamations.length)] +
|
||||
' ' +
|
||||
bimAdjs[Math.floor(Math.random() * bimAdjs.length)] +
|
||||
' ' +
|
||||
bimNouns[Math.floor(Math.random() * bimNouns.length)]
|
||||
let field = {
|
||||
key: `placeholder ${crs({ length: 6 })}`,
|
||||
type: 'field',
|
||||
value: randomPhrase,
|
||||
valid: true,
|
||||
id: crs({ length: 10 })
|
||||
}
|
||||
this.$emit('add-prop', { field: field, path: this.path })
|
||||
},
|
||||
emitRemoveAt(index) {
|
||||
this.$emit('remove-prop', { path: this.path, index: index })
|
||||
},
|
||||
emitFieldToObject(entry, index) {
|
||||
let obj = {
|
||||
key: entry.key,
|
||||
type: 'object',
|
||||
id: entry.id,
|
||||
valid: entry.valid,
|
||||
globals: [
|
||||
{
|
||||
key: `placeholder ${crs({ length: 6 })}`,
|
||||
type: 'field',
|
||||
value: entry.value,
|
||||
id: crs({ length: 10 }),
|
||||
valid: true
|
||||
}
|
||||
]
|
||||
}
|
||||
this.$emit('field-to-object', { obj: obj, path: this.path, index: index })
|
||||
},
|
||||
emitObjectToField(entry, index) {
|
||||
let fields = entry.globals
|
||||
this.$emit('object-to-field', { fields: fields, path: this.path, index: index })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.v-card {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.v-card__title {
|
||||
font-weight: 500;
|
||||
font-size: large;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.v-text-field {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.entry-key {
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
top: 0.6rem;
|
||||
}
|
||||
|
||||
.entry-delete {
|
||||
position: relative;
|
||||
top: -0.2rem;
|
||||
}
|
||||
|
||||
.dragArea {
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.flip-list-move {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-list-item :to="`/streams/${streamId}/commits/${commit.id}`">
|
||||
<v-list-item :to="route ? route : `/streams/${streamId}/commits/${commit.id}`">
|
||||
<v-list-item-icon>
|
||||
<user-avatar
|
||||
:id="commit.authorId"
|
||||
@@ -36,7 +36,7 @@ import SourceAppAvatar from './SourceAppAvatar'
|
||||
|
||||
export default {
|
||||
components: { UserAvatar, SourceAppAvatar },
|
||||
props: ['commit', 'streamId'],
|
||||
props: ['commit', 'streamId', 'route'],
|
||||
computed: {
|
||||
commitDate() {
|
||||
if (!this.commit) return null
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
small
|
||||
color="primary"
|
||||
class="pa-2"
|
||||
:to="`/streams/${stream.id}/commits/${stream.commits.items[0].id}`"
|
||||
:to="
|
||||
stream.commits.items[0].branchName.startsWith('globals')
|
||||
? `/streams/${stream.id}/${stream.commits.items[0].branchName}/${stream.commits.items[0].id}`
|
||||
: `/streams/${stream.id}/commits/${stream.commits.items[0].id}`
|
||||
"
|
||||
>
|
||||
<v-icon small class="mr-1">mdi-source-commit</v-icon>
|
||||
{{ stream.commits.items[0].id }}
|
||||
@@ -38,7 +42,11 @@
|
||||
on
|
||||
<router-link
|
||||
class="text-decoration-none"
|
||||
:to="`/streams/${stream.id}/branches/${stream.commits.items[0].branchName}`"
|
||||
:to="
|
||||
stream.commits.items[0].branchName.startsWith('globals')
|
||||
? `/streams/${stream.id}/${stream.commits.items[0].branchName}`
|
||||
: `/streams/${stream.id}/branches/${stream.commits.items[0].branchName}`
|
||||
"
|
||||
>
|
||||
<v-icon small color="primary">mdi-source-branch</v-icon>
|
||||
{{ stream.commits.items[0].branchName }}
|
||||
@@ -93,6 +101,7 @@
|
||||
<v-icon small class="mr-2 float-left">mdi-cog-outline</v-icon>
|
||||
Edit
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="editStreamDialog" max-width="500">
|
||||
<stream-edit-dialog
|
||||
:stream-id="stream.id"
|
||||
@@ -104,6 +113,18 @@
|
||||
</v-dialog>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-show="isHomeRoute">
|
||||
<v-btn
|
||||
v-tooltip="'Edit stream global variables!'"
|
||||
block
|
||||
small
|
||||
elevation="0"
|
||||
:to="`/streams/${stream.id}/globals`"
|
||||
>
|
||||
Globals
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-title v-show="isHomeRoute"><h5>Collaborators</h5></v-card-title>
|
||||
<v-card-text v-show="isHomeRoute">
|
||||
<v-row no-gutters>
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
<div 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">
|
||||
<v-avatar
|
||||
v-if="user"
|
||||
class="ma-1"
|
||||
color="grey lighten-3"
|
||||
:size="size"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-img v-if="avatar" :src="avatar" />
|
||||
<v-img v-else :src="`https://robohash.org/` + id + `.png?size=40x40`" />
|
||||
</v-avatar>
|
||||
<v-avatar v-else class="ma-1" :size="size" v-bind="attrs" v-on="on">
|
||||
<v-img contain src="/logo.svg"></v-img>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-card style="width: 200px" :to="isSelf ? '/profile' : '/profile/' + id">
|
||||
<v-card v-if="user" style="width: 200px" :to="isSelf ? '/profile' : '/profile/' + id">
|
||||
<v-card-text v-if="!$apollo.loading" class="text-center">
|
||||
<v-avatar class="my-4" color="grey lighten-3" :size="40">
|
||||
<v-img v-if="avatar" :src="avatar" />
|
||||
@@ -24,6 +34,13 @@
|
||||
<br />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card v-else>
|
||||
<v-card-text class="text-xs">
|
||||
<b>Speckle Ghost</b>
|
||||
<br />
|
||||
This user no longer exists.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-avatar v-else class="ma-1" color="grey lighten-3" :size="size">
|
||||
<v-img v-if="avatar" :src="avatar" />
|
||||
|
||||
@@ -1,74 +1,64 @@
|
||||
<template>
|
||||
<v-card :loading="loading">
|
||||
<template slot="progress">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</template>
|
||||
<div v-if="branch.name !== 'main'">
|
||||
<v-card-title>Edit Branch</v-card-title>
|
||||
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="updateBranch">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
label="Name"
|
||||
:rules="nameRules"
|
||||
validate-on-blur
|
||||
required
|
||||
autofocus
|
||||
></v-text-field>
|
||||
<v-textarea v-model="description" rows="2" label="Description"></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" block :disabled="!valid" type="submit">Save</v-btn>
|
||||
<v-dialog v-model="show" width="500" @keydown.esc="cancel">
|
||||
<v-card :loading="loading" class="pa-4">
|
||||
<template slot="progress">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</template>
|
||||
<div v-if="branch && branch.name !== 'main'">
|
||||
<v-card-title>Edit Branch</v-card-title>
|
||||
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="agree">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="branch.name"
|
||||
label="Name"
|
||||
:rules="nameRules"
|
||||
validate-on-blur
|
||||
required
|
||||
autofocus
|
||||
></v-text-field>
|
||||
<v-textarea v-model="branch.description" rows="2" label="Description"></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="cancel">Cancel</v-btn>
|
||||
<v-btn color="primary" text :disabled="!valid" type="submit">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
<v-card-actions class="error--text body-2 pa-2">
|
||||
<v-btn block x-small text color="error" @click="showDelete = true">Delete Branch</v-btn>
|
||||
<v-dialog v-model="showDelete" max-width="500">
|
||||
<v-card>
|
||||
<v-card-title>Are you sure?</v-card-title>
|
||||
<v-card-text>
|
||||
You cannot undo this action. The branch
|
||||
<b>{{ branch.name }}</b>
|
||||
will be permanently deleted.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showDelete = false">Cancel</v-btn>
|
||||
<v-btn color="error" text @click="deleteBranch">Delete</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
<v-card-actions class="error--text body-2 pa-2">
|
||||
<v-btn block x-small text color="error" @click="showDelete = true">Delete Branch</v-btn>
|
||||
<v-dialog v-model="showDelete" max-width="500">
|
||||
<v-card>
|
||||
<v-card-title>Are you sure?</v-card-title>
|
||||
<v-card-text>
|
||||
You cannot undo this action. The branch
|
||||
<b>{{ name }}</b>
|
||||
will be permanently deleted.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showDelete = false">Cancel</v-btn>
|
||||
<v-btn color="error" text @click="deleteBranch">Delete</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-card-text>You cannot edit the main branch.</v-card-text>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-card-text>You cannot edit the main branch.</v-card-text>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
streamId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
branch: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
name: null,
|
||||
description: null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
branch: null,
|
||||
valid: true,
|
||||
loading: false,
|
||||
name: this.branch.name,
|
||||
showDelete: false,
|
||||
nameRules: [
|
||||
(v) => !!v || 'Branches need a name too!',
|
||||
@@ -78,7 +68,6 @@ export default {
|
||||
(v) => (v && v.length <= 100) || 'Name must be less than 100 characters',
|
||||
(v) => (v && v.length >= 3) || 'Name must be at least 3 characters'
|
||||
],
|
||||
description: this.branch.description,
|
||||
isEdit: false,
|
||||
pendingDelete: false,
|
||||
allBranchNames: []
|
||||
@@ -104,13 +93,26 @@ export default {
|
||||
}
|
||||
},
|
||||
update(data) {
|
||||
return data.stream.branches.items
|
||||
.map((b) => b.name)
|
||||
.filter((name) => name !== this.branch.name)
|
||||
return data.stream.branches.items.filter((b) => b.name !== this.branch.name)
|
||||
},
|
||||
skip() {
|
||||
return this.branch == null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.dialog
|
||||
},
|
||||
set(value) {
|
||||
this.dialog = value
|
||||
if (value === false) {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
async deleteBranch() {
|
||||
this.loading = true
|
||||
@@ -132,10 +134,27 @@ export default {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
this.$emit('close', { deleted: true })
|
||||
|
||||
this.loading = false
|
||||
|
||||
this.resolve({
|
||||
result: true,
|
||||
deleted: true
|
||||
})
|
||||
this.dialog = false
|
||||
},
|
||||
async updateBranch() {
|
||||
open(branch) {
|
||||
this.dialog = true
|
||||
if (this.$refs.form) this.$refs.form.resetValidation()
|
||||
|
||||
this.branch = { ...branch }
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve
|
||||
this.reject = reject
|
||||
})
|
||||
},
|
||||
async agree() {
|
||||
if (!this.$refs.form.validate()) return
|
||||
|
||||
this.loading = true
|
||||
@@ -150,13 +169,25 @@ export default {
|
||||
params: {
|
||||
streamId: this.$route.params.streamId,
|
||||
id: this.branch.id,
|
||||
name: this.name,
|
||||
description: this.description
|
||||
name: this.branch.name,
|
||||
description: this.branch.description
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
this.$emit('close', { name: this.name })
|
||||
|
||||
this.resolve({
|
||||
result: true,
|
||||
name: this.branch.name
|
||||
})
|
||||
this.dialog = false
|
||||
},
|
||||
cancel() {
|
||||
this.resolve({
|
||||
result: false
|
||||
})
|
||||
this.dialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
<template>
|
||||
<v-card :loading="loading">
|
||||
<template slot="progress">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</template>
|
||||
<v-card-title>New Branch</v-card-title>
|
||||
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="createBranch">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
label="Name"
|
||||
:rules="nameRules"
|
||||
validate-on-blur
|
||||
required
|
||||
autofocus
|
||||
></v-text-field>
|
||||
<v-textarea v-model="description" rows="2" label="Description"></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text :disabled="!valid" type="submit">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
<v-dialog v-model="show" width="500" @keydown.esc="cancel">
|
||||
<v-card :loading="loading">
|
||||
<template slot="progress">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</template>
|
||||
<v-card-title>New Branch</v-card-title>
|
||||
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="agree">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
label="Name"
|
||||
:rules="nameRules"
|
||||
validate-on-blur
|
||||
required
|
||||
autofocus
|
||||
></v-text-field>
|
||||
<v-textarea v-model="description" rows="2" label="Description"></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text :disabled="!valid" type="submit">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
streamId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
branchNames: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
streamId: null,
|
||||
branchNames: [],
|
||||
valid: false,
|
||||
loading: false,
|
||||
name: null,
|
||||
nameRules: [
|
||||
(v) => !!v || 'Branches need a name too!',
|
||||
(v) =>
|
||||
(v && !v.startsWith('globals')) ||
|
||||
'Globals is a reserved branch name. Please choose a different name.',
|
||||
(v) =>
|
||||
(v && this.branchNames.findIndex((e) => e === v) === -1) ||
|
||||
'A branch with this name already exists',
|
||||
@@ -55,9 +53,33 @@ export default {
|
||||
pendingDelete: false
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.dialog
|
||||
},
|
||||
set(value) {
|
||||
this.dialog = value
|
||||
if (value === false) {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async createBranch() {
|
||||
open(streamId, branchNames) {
|
||||
this.dialog = true
|
||||
if (this.$refs.form) this.$refs.form.resetValidation()
|
||||
|
||||
this.branchNames = branchNames
|
||||
this.streamId = streamId
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve
|
||||
this.reject = reject
|
||||
})
|
||||
},
|
||||
async agree() {
|
||||
if (!this.$refs.form.validate()) return
|
||||
|
||||
this.loading = true
|
||||
@@ -77,8 +99,19 @@ export default {
|
||||
}
|
||||
})
|
||||
this.loading = false
|
||||
this.$emit('close')
|
||||
|
||||
this.resolve({
|
||||
result: true,
|
||||
name: this.name
|
||||
})
|
||||
this.dialog = false
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
this.resolve({
|
||||
result: false
|
||||
})
|
||||
this.dialog = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<v-card :loading="loading">
|
||||
<template slot="progress">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</template>
|
||||
<v-card-title>Save Globals</v-card-title>
|
||||
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="saveGlobals">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="message"
|
||||
label="Message"
|
||||
:rules="nameRules"
|
||||
validate-on-blur
|
||||
required
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text :disabled="!valid" type="submit">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
streamId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
commitObj: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
branchName: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
valid: false,
|
||||
loading: false,
|
||||
name: null,
|
||||
nameRules: [(v) => (v && v.length >= 3) || 'Message must be at least 3 characters'],
|
||||
message: null
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
async saveGlobals() {
|
||||
if (!this.$refs.form.validate()) return
|
||||
|
||||
this.loading = true
|
||||
this.$matomo && this.$matomo.trackPageView('globals/save')
|
||||
let res = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ObjectCreate($params: ObjectCreateInput!) {
|
||||
objectCreate(objectInput: $params)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
params: {
|
||||
streamId: this.streamId,
|
||||
objects: [this.commitObj]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation CommitCreate($commit: CommitCreateInput!) {
|
||||
commitCreate(commit: $commit)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
commit: {
|
||||
streamId: this.streamId,
|
||||
branchName: this.branchName,
|
||||
objectId: res.data.objectCreate[0],
|
||||
message: this.message,
|
||||
sourceApplication: 'web'
|
||||
}
|
||||
}
|
||||
})
|
||||
this.loading = false
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -20,7 +20,11 @@
|
||||
:rules="validation.emailRules"
|
||||
label="email"
|
||||
></v-text-field>
|
||||
<v-text-field v-model="message" label="message"></v-text-field>
|
||||
<v-text-field
|
||||
v-model="message"
|
||||
:rules="validation.messageRules"
|
||||
label="message"
|
||||
></v-text-field>
|
||||
<v-card-actions>
|
||||
<v-btn block color="primary" type="submit">Send invite</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -32,6 +36,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
export default {
|
||||
name: 'ServerInviteDialog',
|
||||
@@ -48,6 +53,17 @@ export default {
|
||||
emailRules: [
|
||||
(v) => !!v || 'E-mail is required',
|
||||
(v) => /.+@.+\..+/.test(v) || 'E-mail must be valid'
|
||||
],
|
||||
messageRules: [
|
||||
(v) => {
|
||||
if (v.length >= 1024) return 'Message too long!'
|
||||
return true
|
||||
},
|
||||
(v) => {
|
||||
let pure = DOMPurify.sanitize(v)
|
||||
if (pure !== v) return 'No crazy hacks please.'
|
||||
else return true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
:rules="validation.emailRules"
|
||||
label="email"
|
||||
></v-text-field>
|
||||
<v-text-field v-model="message" label="message"></v-text-field>
|
||||
<v-text-field
|
||||
v-model="message"
|
||||
:rules="validation.messageRules"
|
||||
label="message"
|
||||
></v-text-field>
|
||||
<v-card-actions>
|
||||
<v-btn block color="primary" type="submit">Send invite</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -33,6 +37,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
export default {
|
||||
name: 'StreamInviteDialog',
|
||||
@@ -55,6 +60,17 @@ export default {
|
||||
emailRules: [
|
||||
(v) => !!v || 'E-mail is required',
|
||||
(v) => /.+@.+\..+/.test(v) || 'E-mail must be valid'
|
||||
],
|
||||
messageRules: [
|
||||
(v) => {
|
||||
if (v.length >= 1024) return 'Message too long!'
|
||||
return true
|
||||
},
|
||||
(v) => {
|
||||
let pure = DOMPurify.sanitize(v)
|
||||
if (pure !== v) return 'No crazy hacks please.'
|
||||
else return true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ query Streams($cursor: String) {
|
||||
createdAt
|
||||
message
|
||||
authorId
|
||||
branchName
|
||||
authorName
|
||||
authorAvatar
|
||||
referencedObject
|
||||
|
||||
@@ -83,9 +83,25 @@ const routes = [
|
||||
},
|
||||
component: () => import('../views/StreamMain.vue')
|
||||
},
|
||||
{
|
||||
path: 'globals/',
|
||||
name: 'globals',
|
||||
meta: {
|
||||
title: 'Globals | Speckle'
|
||||
},
|
||||
component: () => import('../views/Globals.vue')
|
||||
},
|
||||
{
|
||||
path: 'globals/:commitId',
|
||||
name: 'previous globals',
|
||||
meta: {
|
||||
title: 'Globals | Speckle'
|
||||
},
|
||||
component: () => import('../views/Globals.vue')
|
||||
},
|
||||
{
|
||||
path: 'branches/',
|
||||
name: 'branchs',
|
||||
name: 'branches',
|
||||
meta: {
|
||||
title: 'Branches | Speckle'
|
||||
},
|
||||
|
||||
@@ -5,13 +5,8 @@
|
||||
<v-skeleton-loader type="article"></v-skeleton-loader>
|
||||
</v-card>
|
||||
<v-card v-else rounded="lg" class="pa-4 mb-4" elevation="0">
|
||||
<v-dialog v-model="dialogBranch" max-width="500">
|
||||
<new-branch-dialog
|
||||
:branch-names="branches.items.map((b) => b.name)"
|
||||
:stream-id="$route.params.streamId"
|
||||
@close="closeBranchDialog"
|
||||
/>
|
||||
</v-dialog>
|
||||
<branch-new-dialog ref="newBranchDialog" />
|
||||
|
||||
<v-card-title>
|
||||
<v-icon class="mr-2">mdi-source-branch</v-icon>
|
||||
Branches
|
||||
@@ -24,7 +19,7 @@
|
||||
text
|
||||
class="px-0"
|
||||
small
|
||||
@click="dialogBranch = true"
|
||||
@click="newBranch"
|
||||
>
|
||||
<v-icon small class="mr-2 float-left">mdi-plus-circle-outline</v-icon>
|
||||
New branch
|
||||
@@ -40,10 +35,10 @@
|
||||
</v-card>
|
||||
|
||||
<v-card v-if="!$apollo.queries.stream.loading" class="mt-5 pa-4" elevation="0" rounded="lg">
|
||||
<v-subheader class="text-uppercase">Branches ({{ branches.items.length }})</v-subheader>
|
||||
<v-subheader class="text-uppercase">Branches ({{ branches.length }})</v-subheader>
|
||||
<v-card-text>
|
||||
<v-list two-line color="transparent">
|
||||
<template v-for="item in branches.items">
|
||||
<template v-for="item in branches">
|
||||
<v-list-item
|
||||
:key="item.id"
|
||||
:to="`/streams/${$route.params.streamId}/branches/${encodeURIComponent(item.name)}`"
|
||||
@@ -71,14 +66,14 @@
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
import NewBranchDialog from '../components/dialogs/BranchNewDialog'
|
||||
import BranchNewDialog from '../components/dialogs/BranchNewDialog'
|
||||
import streamBranchesQuery from '../graphql/streamBranches.gql'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'StreamMain',
|
||||
components: {
|
||||
NewBranchDialog
|
||||
BranchNewDialog
|
||||
},
|
||||
props: {
|
||||
userRole: {
|
||||
@@ -87,9 +82,7 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogBranch: false
|
||||
}
|
||||
return {}
|
||||
},
|
||||
apollo: {
|
||||
stream: {
|
||||
@@ -144,7 +137,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
branches() {
|
||||
return this.stream.branches
|
||||
return this.stream.branches.items.filter((b) => !b.name.startsWith('globals'))
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
@@ -170,9 +163,18 @@ export default {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
},
|
||||
methods: {
|
||||
closeBranchDialog() {
|
||||
this.dialogBranch = false
|
||||
this.$apollo.queries.stream.refetch()
|
||||
newBranch() {
|
||||
this.$refs.newBranchDialog
|
||||
.open(
|
||||
this.$route.params.streamId,
|
||||
this.branches.map((b) => b.name)
|
||||
)
|
||||
.then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
else {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
</v-col>
|
||||
<v-col v-else-if="stream.branch" cols="12">
|
||||
<v-card class="pa-4" elevation="0" rounded="lg">
|
||||
<v-dialog v-model="dialogEdit" max-width="500">
|
||||
<branch-edit-dialog :branch="stream.branch" @close="closeEdit" />
|
||||
</v-dialog>
|
||||
<branch-edit-dialog ref="editBranchDialog" />
|
||||
|
||||
<v-card-title class="mr-8">
|
||||
<v-icon class="mr-2">mdi-source-branch</v-icon>
|
||||
<span class="d-inline-block">{{ stream.branch.name }}</span>
|
||||
@@ -20,7 +19,7 @@
|
||||
color="primary"
|
||||
text
|
||||
class="px-0"
|
||||
@click="dialogEdit = true"
|
||||
@click="editBranch"
|
||||
>
|
||||
<v-icon small class="mr-2 float-left">mdi-cog-outline</v-icon>
|
||||
Edit branch
|
||||
@@ -144,19 +143,21 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeEdit({ name, deleted }) {
|
||||
this.dialogEdit = false
|
||||
if (deleted) {
|
||||
this.$router.push({ path: `/streams/${this.streamId}` })
|
||||
return
|
||||
}
|
||||
if (name !== this.$route.params.branchName) {
|
||||
this.$router.push({
|
||||
path: `/streams/${this.streamId}/branches/${encodeURIComponent(name)}/commits`
|
||||
})
|
||||
return
|
||||
}
|
||||
this.$apollo.queries.stream.refetch()
|
||||
editBranch() {
|
||||
this.$refs.editBranchDialog.open(this.stream.branch).then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
else if (dialog.deleted) {
|
||||
this.$router.push({ path: `/streams/${this.streamId}` })
|
||||
} else if (dialog.name !== this.$route.params.branchName) {
|
||||
//this.$router.push does not work, refresh entire window
|
||||
|
||||
this.$router.push({
|
||||
path: `/streams/${this.streamId}/branches/${encodeURIComponent(dialog.name)}/commits`
|
||||
})
|
||||
} else {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<div v-if="!objectId && !$apollo.loading && !revealBuilder">
|
||||
<v-card :loading="loading">
|
||||
<template slot="progress">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</template>
|
||||
<v-card-title>You don't have any globals on this stream!</v-card-title>
|
||||
<v-card-text class="subtitle-1">
|
||||
Globals are useful for storing design values, project requirements, notes, or any info you
|
||||
want to keep track of alongside your geometry. Would you like to create some now?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" @click="createClicked">create globals</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
<div v-if="objectId || revealBuilder">
|
||||
<globals-builder
|
||||
:branch-name="branchName"
|
||||
:stream-id="streamId"
|
||||
:object-id="objectId"
|
||||
:commit-message="commit ? commit.message : null"
|
||||
:user-role="$attrs['user-role']"
|
||||
@new-commit="newCommit"
|
||||
/>
|
||||
<v-card v-if="!$apollo.loading && branch.commits.items.length">
|
||||
<v-card-title>History</v-card-title>
|
||||
<v-card-text>
|
||||
<list-item-commit
|
||||
v-for="item in branch.commits.items"
|
||||
:key="item.id"
|
||||
:route="`/streams/${streamId}/globals/${item.id}`"
|
||||
:commit="item"
|
||||
:stream-id="streamId"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import branchQuery from '../graphql/branch.gql'
|
||||
|
||||
export default {
|
||||
name: 'Globals',
|
||||
components: {
|
||||
GlobalsBuilder: () => import('../components/GlobalsBuilder'),
|
||||
ListItemCommit: () => import('../components/ListItemCommit')
|
||||
},
|
||||
apollo: {
|
||||
branch: {
|
||||
query: branchQuery,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.streamId,
|
||||
branchName: this.branchName
|
||||
}
|
||||
},
|
||||
update(data) {
|
||||
return data.stream.branch
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
branchName: 'globals', //TODO: handle multipile globals branches,
|
||||
|
||||
revealBuilder: false,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
streamId() {
|
||||
return this.$route.params.streamId
|
||||
},
|
||||
commit() {
|
||||
return this.$route.params.commitId
|
||||
? this.branch?.commits?.items?.filter((c) => c.id == this.$route.params.commitId)[0]
|
||||
: this.branch?.commits?.items[0]
|
||||
},
|
||||
objectId() {
|
||||
return this.commit?.referencedObject
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async createClicked() {
|
||||
if (!this.branch) {
|
||||
this.loading = true
|
||||
this.$matomo && this.$matomo.trackPageView('globals/branch/create')
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation branchCreate($params: BranchCreateInput!) {
|
||||
branchCreate(branch: $params)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
params: {
|
||||
streamId: this.streamId,
|
||||
name: 'globals',
|
||||
description: 'Stream globals'
|
||||
}
|
||||
}
|
||||
})
|
||||
this.$apollo.queries.branch.refetch()
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
this.revealBuilder = true
|
||||
},
|
||||
newCommit() {
|
||||
this.$apollo.queries.branch.refetch()
|
||||
if (this.$route.params.commitId) this.$router.push(`/streams/${this.streamId}/globals`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -9,7 +9,7 @@
|
||||
<v-select
|
||||
v-if="branches"
|
||||
v-model="selectedBranch"
|
||||
:items="branches.items"
|
||||
:items="branches"
|
||||
item-value="name"
|
||||
solo
|
||||
flat
|
||||
@@ -41,8 +41,84 @@
|
||||
:to="'/streams/' + $route.params.streamId + '/branches'"
|
||||
>
|
||||
<v-icon class="mr-2 float-left">mdi-source-branch</v-icon>
|
||||
{{ branches.totalCount }} branch{{ branches.totalCount > 1 ? 'es' : '' }}
|
||||
{{ branches.length }} branch{{ branches.length > 1 ? 'es' : '' }}
|
||||
</v-btn>
|
||||
|
||||
<!-- DIALOGS -->
|
||||
<branch-new-dialog ref="newBranchDialog" />
|
||||
<branch-edit-dialog ref="editBranchDialog" />
|
||||
|
||||
<!-- MENU -->
|
||||
|
||||
<v-menu
|
||||
v-if="userRole === 'contributor' || userRole === 'owner'"
|
||||
offset-y
|
||||
class="mx-2 mb-5"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
style="position: absolute; top: 36px; right: 20px"
|
||||
color="primary"
|
||||
v-bind="attrs"
|
||||
icon
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="newBranch">
|
||||
<v-list-item-action class="mr-2">
|
||||
<v-icon>mdi-plus-circle-outline</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>New branch</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="selectedBranch && selectedBranch.name != 'main'"
|
||||
@click="editBranch"
|
||||
>
|
||||
<v-list-item-action class="mr-2">
|
||||
<v-icon>mdi-cog-outline</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Edit {{ selectedBranch.name }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<div
|
||||
v-if="
|
||||
stream &&
|
||||
stream.commit &&
|
||||
stream.commit.branchName != 'main' &&
|
||||
stream.commit.branchName != selectedBranch.name
|
||||
"
|
||||
class="pb-2 caption"
|
||||
>
|
||||
<v-alert color="primary" class="caption" dense text type="info">
|
||||
The last commit of this stream is on the
|
||||
<v-btn
|
||||
text
|
||||
x-small
|
||||
color="primary darken-1"
|
||||
:to="'/streams/' + $route.params.streamId + '/branches/' + stream.commit.branchName"
|
||||
>
|
||||
{{ stream.commit.branchName }}
|
||||
</v-btn>
|
||||
branch, see
|
||||
<v-btn
|
||||
x-small
|
||||
text
|
||||
color="primary darken-1"
|
||||
:to="'/streams/' + $route.params.streamId + '/commits/' + stream.commit.id"
|
||||
>
|
||||
{{ stream.commit.message }}
|
||||
</v-btn>
|
||||
</v-alert>
|
||||
</div>
|
||||
</v-sheet>
|
||||
|
||||
<div v-if="latestCommit" style="height: 50vh">
|
||||
@@ -152,7 +228,7 @@
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-card-title>
|
||||
Description
|
||||
Stream Description
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="userRole === 'owner'"
|
||||
@@ -193,6 +269,8 @@ import streamBranchesQuery from '../graphql/streamBranches.gql'
|
||||
import Renderer from '../components/Renderer'
|
||||
import UserAvatar from '../components/UserAvatar'
|
||||
import ErrorBlock from '../components/ErrorBlock'
|
||||
import BranchNewDialog from '../components/dialogs/BranchNewDialog'
|
||||
import BranchEditDialog from '../components/dialogs/BranchEditDialog'
|
||||
|
||||
export default {
|
||||
name: 'StreamMain',
|
||||
@@ -202,7 +280,9 @@ export default {
|
||||
SourceAppAvatar,
|
||||
NoDataPlaceholder,
|
||||
Renderer,
|
||||
ErrorBlock
|
||||
ErrorBlock,
|
||||
BranchNewDialog,
|
||||
BranchEditDialog
|
||||
},
|
||||
props: {
|
||||
userRole: {
|
||||
@@ -213,10 +293,11 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
dialogDescription: false,
|
||||
dialogBranch: false,
|
||||
selectedBranch: null,
|
||||
clearRendererTrigger: 0,
|
||||
error: ''
|
||||
error: '',
|
||||
dialogBranchNew: false,
|
||||
dialogBranchEdit: false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
@@ -228,7 +309,7 @@ export default {
|
||||
}
|
||||
},
|
||||
update(data) {
|
||||
return data.stream.branches
|
||||
return data.stream.branches.items.filter((b) => !b.name.startsWith('globals'))
|
||||
}
|
||||
},
|
||||
description: {
|
||||
@@ -247,6 +328,26 @@ export default {
|
||||
},
|
||||
update: (data) => data.stream.description
|
||||
},
|
||||
stream: {
|
||||
query: gql`
|
||||
query($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
commit {
|
||||
branchName
|
||||
id
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.streamId
|
||||
}
|
||||
}
|
||||
//update: (data) => data.stream.description
|
||||
},
|
||||
$subscribe: {
|
||||
branchCreated: {
|
||||
query: gql`
|
||||
@@ -297,7 +398,7 @@ export default {
|
||||
},
|
||||
branchNames() {
|
||||
if (!this.branches) return []
|
||||
return this.branches.items.map((b) => b.name)
|
||||
return this.branches.map((b) => b.name)
|
||||
},
|
||||
compiledStreamDescription() {
|
||||
if (!this.description) return ''
|
||||
@@ -337,15 +438,48 @@ export default {
|
||||
this.dialogDescription = false
|
||||
this.$apollo.queries.description.refetch()
|
||||
},
|
||||
closeBranchDialog() {
|
||||
this.dialogBranch = false
|
||||
this.$apollo.queries.branches.refetch()
|
||||
editBranch() {
|
||||
this.$refs.editBranchDialog.open(this.selectedBranch).then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
else if (dialog.deleted) {
|
||||
this.$router.push({ path: `/streams/${this.$route.params.streamId}` })
|
||||
} else if (dialog.name !== this.selectedBranch.name) {
|
||||
//this.$router.push does not work, refresh entire window
|
||||
window.location =
|
||||
window.origin +
|
||||
'/streams/' +
|
||||
this.$route.params.streamId +
|
||||
'/branches/' +
|
||||
encodeURIComponent(dialog.name)
|
||||
} else {
|
||||
this.$apollo.queries.branches.refetch()
|
||||
}
|
||||
})
|
||||
},
|
||||
newBranch() {
|
||||
this.$refs.newBranchDialog
|
||||
.open(
|
||||
this.$route.params.streamId,
|
||||
this.branches.map((b) => b.name)
|
||||
)
|
||||
.then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
else {
|
||||
//this.$router.push does not work, refresh entire window
|
||||
window.location =
|
||||
window.origin +
|
||||
'/streams/' +
|
||||
this.$route.params.streamId +
|
||||
'/branches/' +
|
||||
encodeURIComponent(dialog.name)
|
||||
}
|
||||
})
|
||||
},
|
||||
selectBranch() {
|
||||
if (!this.branches) return
|
||||
let branchName = this.$route.params.branchName ? this.$route.params.branchName : 'main'
|
||||
let index = this.branches.items.findIndex((x) => x.name === branchName)
|
||||
if (index > -1) this.selectedBranch = this.branches.items[index]
|
||||
let index = this.branches.findIndex((x) => x.name === branchName)
|
||||
if (index > -1) this.selectedBranch = this.branches[index]
|
||||
else this.error = 'Branch ' + branchName + ' does not exist'
|
||||
},
|
||||
changeBranch() {
|
||||
|
||||
@@ -39,7 +39,13 @@
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="subtitle-2">
|
||||
<router-link :to="'streams/' + a.streamId + '/commits/' + a.id">
|
||||
<router-link
|
||||
:to="
|
||||
a.branchName.startsWith('globals')
|
||||
? `streams/${a.streamId}/${a.branchName}/${a.id}`
|
||||
: `streams/${a.streamId}/commits/${a.id}`
|
||||
"
|
||||
>
|
||||
{{ a.message }}
|
||||
</router-link>
|
||||
</v-list-item-title>
|
||||
|
||||
@@ -40,7 +40,9 @@ module.exports = {
|
||||
Branch: {
|
||||
|
||||
async author( parent, args, context, info ) {
|
||||
return await getUserById( { userId: parent.authorId } )
|
||||
if ( parent.userId )
|
||||
return await getUserById( { userId: parent.authorId } )
|
||||
else return null
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -40,8 +40,8 @@ module.exports = {
|
||||
if ( args.limit && args.limit > 100 )
|
||||
throw new UserInputError( 'Cannot return more than 100 items, please use pagination.' )
|
||||
|
||||
let {cursor, users} = await searchUsers( args.query, args.limit, args.cursor )
|
||||
return {cursor: cursor, items: users}
|
||||
let { cursor, users } = await searchUsers( args.query, args.limit, args.cursor )
|
||||
return { cursor: cursor, items: users }
|
||||
},
|
||||
|
||||
async userPwdStrength( parent, args, context, info ) {
|
||||
|
||||
@@ -12,7 +12,7 @@ extend type User {
|
||||
type Branch {
|
||||
id: String!
|
||||
name: String!
|
||||
author: User!
|
||||
author: User
|
||||
description: String
|
||||
commits(limit: Int! = 25, cursor: String): CommitCollection
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// /* istanbul ignore file */
|
||||
exports.up = async ( knex ) => {
|
||||
await knex.raw( 'ALTER TABLE commits ALTER COLUMN "author" DROP NOT NULL;' )
|
||||
await knex.raw( 'ALTER TABLE commits DROP CONSTRAINT commits_author_foreign;' )
|
||||
await knex.raw( `
|
||||
ALTER TABLE commits
|
||||
ADD CONSTRAINT commits_author_foreign
|
||||
FOREIGN KEY (author)
|
||||
REFERENCES users(id)
|
||||
ON DELETE SET NULL;
|
||||
` )
|
||||
|
||||
await knex.raw( 'ALTER TABLE branches DROP CONSTRAINT branches_authorid_foreign;' )
|
||||
await knex.raw( `
|
||||
ALTER TABLE branches
|
||||
ADD CONSTRAINT branches_authorid_foreign
|
||||
FOREIGN KEY ("authorId")
|
||||
REFERENCES users(id)
|
||||
ON DELETE SET NULL;
|
||||
` )
|
||||
}
|
||||
|
||||
exports.down = async ( knex ) => {
|
||||
// NOTE:
|
||||
// This migration cannot run backwards: if a user deletes their account, the previous not null
|
||||
// constraint cannot be satisfied. Therefore, there's no going back (and there isn't really a need either).
|
||||
|
||||
// await knex.raw( 'ALTER TABLE branches DROP CONSTRAINT branches_authorid_foreign;' )
|
||||
// await knex.raw( `
|
||||
// ALTER TABLE branches
|
||||
// ADD CONSTRAINT branches_authorid_foreign
|
||||
// FOREIGN KEY ("authorId")
|
||||
// REFERENCES users(id);
|
||||
// ` )
|
||||
|
||||
// await knex.raw( 'ALTER TABLE commits DROP CONSTRAINT commits_author_foreign;' )
|
||||
// await knex.raw( `
|
||||
// ALTER TABLE commits
|
||||
// ADD CONSTRAINT commits_author_foreign
|
||||
// FOREIGN KEY (author)
|
||||
// REFERENCES users(id);
|
||||
// ` )
|
||||
// await knex.raw( 'ALTER TABLE commits ALTER COLUMN "author" SET NOT NULL;' )
|
||||
}
|
||||
@@ -8,9 +8,11 @@ const cors = require( 'cors' )
|
||||
const { matomoMiddleware } = require( `${appRoot}/logging/matomoHelper` )
|
||||
const { contextMiddleware, validateScopes, authorizeResolver } = require( `${appRoot}/modules/shared` )
|
||||
const { validatePermissionsReadStream } = require( './authUtils' )
|
||||
|
||||
const { SpeckleObjectsStream } = require( './speckleObjectsStream' )
|
||||
const { getObjectsStream } = require( '../services/objects' )
|
||||
|
||||
const { pipeline } = require( 'stream' )
|
||||
|
||||
module.exports = ( app ) => {
|
||||
|
||||
app.options( '/api/getobjects/:streamId', cors() )
|
||||
@@ -24,72 +26,25 @@ module.exports = ( app ) => {
|
||||
|
||||
let simpleText = req.headers.accept === 'text/plain'
|
||||
|
||||
let dbStream = await getObjectsStream( { streamId: req.params.streamId, objectIds: childrenList } )
|
||||
|
||||
let currentChunkSize = 0
|
||||
let maxChunkSize = 50000
|
||||
let chunk = simpleText ? '' : [ ]
|
||||
let isFirstBuffer = true
|
||||
|
||||
res.writeHead( 200, { 'Content-Encoding': 'gzip', 'Content-Type': simpleText ? 'text/plain' : 'application/json' } )
|
||||
|
||||
const gzip = zlib.createGzip( )
|
||||
let dbStream = await getObjectsStream( { streamId: req.params.streamId, objectIds: childrenList } )
|
||||
let speckleObjStream = new SpeckleObjectsStream( simpleText )
|
||||
let gzipStream = zlib.createGzip( )
|
||||
|
||||
if ( !simpleText ) gzip.write( '[' )
|
||||
|
||||
// helper func to flush the gzip buffer
|
||||
const writeBuffer = () => {
|
||||
if ( simpleText ) {
|
||||
gzip.write( chunk )
|
||||
} else {
|
||||
if ( !isFirstBuffer ) {
|
||||
gzip.write( ',' )
|
||||
}
|
||||
gzip.write( chunk.join( ',' ) )
|
||||
}
|
||||
gzip.flush( )
|
||||
chunk = simpleText ? '' : [ ]
|
||||
isFirstBuffer = false
|
||||
}
|
||||
|
||||
let k = 0
|
||||
let requestDropped = false
|
||||
dbStream.on( 'data', row => {
|
||||
try {
|
||||
let data = JSON.stringify( row.data )
|
||||
currentChunkSize += Buffer.byteLength( data, 'utf8' )
|
||||
if ( simpleText ) {
|
||||
chunk += `${row.data.id}\t${data}\n`
|
||||
pipeline(
|
||||
dbStream,
|
||||
speckleObjStream,
|
||||
gzipStream,
|
||||
res,
|
||||
( err ) => {
|
||||
if ( err ) {
|
||||
debug( 'speckle:error' )( `[User ${req.context.userId || '-'}] Error streaming objects from stream ${req.params.streamId}: ${err}` )
|
||||
} else {
|
||||
chunk.push( data )
|
||||
debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Streamed ${childrenList.length} objects from stream ${req.params.streamId} (size: ${gzipStream.bytesWritten / 1000000} MB)` )
|
||||
}
|
||||
if ( currentChunkSize >= maxChunkSize ) {
|
||||
currentChunkSize = 0
|
||||
writeBuffer()
|
||||
}
|
||||
k++
|
||||
} catch ( e ) {
|
||||
requestDropped = true
|
||||
debug( 'speckle:error' )( `'Failed to find object, or object is corrupted.' ${req.params.objectId}` )
|
||||
return
|
||||
}
|
||||
} )
|
||||
)
|
||||
|
||||
dbStream.on( 'error', err => {
|
||||
debug( 'speckle:error' )( `Error in streaming object children for ${req.params.objectId}: ${err}` )
|
||||
requestDropped = true
|
||||
return
|
||||
} )
|
||||
|
||||
dbStream.on( 'end', ( ) => {
|
||||
if ( currentChunkSize !== 0 ) {
|
||||
writeBuffer()
|
||||
}
|
||||
if ( !simpleText ) gzip.write( ']' )
|
||||
gzip.end( )
|
||||
} )
|
||||
|
||||
// 🚬
|
||||
gzip.pipe( res )
|
||||
} )
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ module.exports = ( app ) => {
|
||||
|
||||
let objectList = JSON.parse( req.body.objects )
|
||||
|
||||
debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Diffing ${objectList.length} objects for stream ${req.params.streamId}` )
|
||||
|
||||
let response = await hasObjects( { streamId: req.params.streamId, objectIds: objectList } )
|
||||
// console.log(response)
|
||||
res.writeHead( 200, { 'Content-Encoding': 'gzip', 'Content-Type': 'application/json' } )
|
||||
|
||||
@@ -10,6 +10,8 @@ const { contextMiddleware } = require( `${appRoot}/modules/shared` )
|
||||
const { validatePermissionsReadStream } = require( './authUtils' )
|
||||
|
||||
const { getObject, getObjectChildrenStream } = require( '../services/objects' )
|
||||
const { SpeckleObjectsStream } = require( './speckleObjectsStream' )
|
||||
const { pipeline } = require( 'stream' )
|
||||
|
||||
module.exports = ( app ) => {
|
||||
|
||||
@@ -28,85 +30,30 @@ module.exports = ( app ) => {
|
||||
return res.status( 404 ).send( `Failed to find object ${req.params.objectId}.` )
|
||||
}
|
||||
|
||||
obj = obj.data
|
||||
|
||||
let simpleText = req.headers.accept === 'text/plain'
|
||||
|
||||
let dbStream = await getObjectChildrenStream( { streamId: req.params.streamId, objectId: req.params.objectId } )
|
||||
|
||||
let currentChunkSize = 0
|
||||
let maxChunkSize = 50000
|
||||
let chunk = simpleText ? '' : [ ]
|
||||
let isFirst = true
|
||||
|
||||
|
||||
res.writeHead( 200, { 'Content-Encoding': 'gzip', 'Content-Type': simpleText ? 'text/plain' : 'application/json' } )
|
||||
|
||||
const gzip = zlib.createGzip( )
|
||||
let dbStream = await getObjectChildrenStream( { streamId: req.params.streamId, objectId: req.params.objectId } )
|
||||
let speckleObjStream = new SpeckleObjectsStream( simpleText )
|
||||
let gzipStream = zlib.createGzip( )
|
||||
|
||||
if ( !simpleText ) gzip.write( '[' )
|
||||
speckleObjStream.write( obj )
|
||||
|
||||
// helper func to flush the gzip buffer
|
||||
const writeBuffer = ( addStartingComma ) => {
|
||||
if ( simpleText ) {
|
||||
gzip.write( chunk )
|
||||
} else {
|
||||
if ( addStartingComma ) {
|
||||
gzip.write( ',' )
|
||||
}
|
||||
gzip.write( chunk.join( ',' ) )
|
||||
}
|
||||
gzip.flush( )
|
||||
chunk = simpleText ? '' : [ ]
|
||||
}
|
||||
|
||||
var objString = JSON.stringify( obj )
|
||||
if ( simpleText ) {
|
||||
chunk += `${obj.id}\t${objString}\n`
|
||||
} else {
|
||||
chunk.push( objString )
|
||||
}
|
||||
writeBuffer( false )
|
||||
|
||||
let k = 0
|
||||
let requestDropped = false
|
||||
dbStream.on( 'data', row => {
|
||||
try {
|
||||
let data = JSON.stringify( row.data )
|
||||
currentChunkSize += Buffer.byteLength( data, 'utf8' )
|
||||
if ( simpleText ) {
|
||||
chunk += `${row.data.id}\t${data}\n`
|
||||
pipeline(
|
||||
dbStream,
|
||||
speckleObjStream,
|
||||
gzipStream,
|
||||
res,
|
||||
( err ) => {
|
||||
if ( err ) {
|
||||
debug( 'speckle:error' )( `[User ${req.context.userId || '-'}] Error downloading object ${req.params.objectId} from stream ${req.params.streamId}: ${err}` )
|
||||
} else {
|
||||
chunk.push( data )
|
||||
debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Downloaded object ${req.params.objectId} from stream ${req.params.streamId} (size: ${gzipStream.bytesWritten / 1000000} MB)` )
|
||||
}
|
||||
if ( currentChunkSize >= maxChunkSize ) {
|
||||
currentChunkSize = 0
|
||||
writeBuffer( true )
|
||||
}
|
||||
k++
|
||||
} catch ( e ) {
|
||||
requestDropped = true
|
||||
debug( 'speckle:error' )( `'Failed to find object, or object is corrupted.' ${req.params.objectId}` )
|
||||
return
|
||||
}
|
||||
} )
|
||||
)
|
||||
|
||||
dbStream.on( 'error', err => {
|
||||
debug( 'speckle:error' )( `Error in streaming object children for ${req.params.objectId}: ${err}` )
|
||||
requestDropped = true
|
||||
return
|
||||
} )
|
||||
|
||||
dbStream.on( 'end', ( ) => {
|
||||
if ( currentChunkSize !== 0 ) {
|
||||
writeBuffer( true )
|
||||
}
|
||||
if ( !simpleText ) gzip.write( ']' )
|
||||
gzip.end( )
|
||||
} )
|
||||
|
||||
// 🚬
|
||||
gzip.pipe( res )
|
||||
} )
|
||||
|
||||
app.options( '/objects/:streamId/:objectId/single', cors() )
|
||||
@@ -122,6 +69,8 @@ module.exports = ( app ) => {
|
||||
return res.status( 404 ).send( `Failed to find object ${req.params.objectId}.` )
|
||||
}
|
||||
|
||||
debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Downloaded single object ${req.params.objectId} from stream ${req.params.streamId}` )
|
||||
|
||||
res.send( obj.data )
|
||||
} )
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const { Transform } = require( 'stream' )
|
||||
|
||||
// A stream that converts database objects stream to "{id}\t{data_json}\n" stream or a json stream of obj.data fields
|
||||
|
||||
class SpeckleObjectsStream extends Transform {
|
||||
constructor( simpleText ) {
|
||||
super( { writableObjectMode: true } )
|
||||
this.simpleText = simpleText
|
||||
|
||||
if ( !this.simpleText ) this.push( '[' )
|
||||
this.isFirstObject = true
|
||||
}
|
||||
|
||||
_transform( dbObj, encoding, callback ) {
|
||||
try {
|
||||
if ( this.simpleText ) {
|
||||
this.push( `${dbObj.data.id}\t${JSON.stringify( dbObj.data )}\n` )
|
||||
} else {
|
||||
// JSON output
|
||||
if ( !this.isFirstObject ) this.push( ',' )
|
||||
this.push( JSON.stringify( dbObj.data ) )
|
||||
this.isFirstObject = false
|
||||
}
|
||||
callback()
|
||||
} catch ( e ) {
|
||||
callback( e )
|
||||
}
|
||||
}
|
||||
|
||||
_flush( callback ) {
|
||||
if ( !this.simpleText ) this.push( ']' )
|
||||
callback()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.SpeckleObjectsStream = SpeckleObjectsStream
|
||||
@@ -18,8 +18,6 @@ module.exports = ( app ) => {
|
||||
return res.status( hasStreamAccess.status ).end()
|
||||
}
|
||||
|
||||
debug( 'speckle:upload-endpoint' )( 'Upload started' )
|
||||
|
||||
let busboy = new Busboy( { headers: req.headers } )
|
||||
let totalProcessed = 0
|
||||
let last = {}
|
||||
@@ -94,7 +92,6 @@ module.exports = ( app ) => {
|
||||
|
||||
await Promise.all( promises )
|
||||
|
||||
debug( 'speckle:upload-endpoint' )( 'Upload ended' )
|
||||
res.status( 201 ).end( )
|
||||
} )
|
||||
|
||||
|
||||
@@ -63,9 +63,9 @@ module.exports = {
|
||||
let query = await Commits( )
|
||||
.columns( [ { id: 'commits.id' }, 'message', 'referencedObject', 'sourceApplication', 'totalChildrenCount', 'parents', 'commits.createdAt', { branchName: 'branches.name' }, { authorName: 'users.name' }, { authorId: 'users.id' }, { authorAvatar: 'users.avatar' } ] )
|
||||
.select( )
|
||||
.join( 'users', 'commits.author', 'users.id' )
|
||||
.join( 'branch_commits', 'commits.id', 'branch_commits.commitId' )
|
||||
.join( 'branches', 'branches.id', 'branch_commits.branchId' )
|
||||
.leftJoin( 'users', 'commits.author', 'users.id' )
|
||||
.where( { 'commits.id': id } )
|
||||
.first( )
|
||||
return await query
|
||||
@@ -97,8 +97,8 @@ module.exports = {
|
||||
.columns( [ { id: 'commitId' }, 'message', 'referencedObject', 'sourceApplication', 'totalChildrenCount', 'parents', 'commits.createdAt', { branchName: 'branches.name' },{ authorName: 'users.name' }, { authorId: 'users.id' }, { authorAvatar: 'users.avatar' } ] )
|
||||
.select( )
|
||||
.join( 'commits', 'commits.id', 'branch_commits.commitId' )
|
||||
.join( 'users', 'commits.author', 'users.id' )
|
||||
.join( 'branches', 'branches.id', 'branch_commits.branchId' )
|
||||
.leftJoin( 'users', 'commits.author', 'users.id' )
|
||||
.where( 'branchId', branchId )
|
||||
|
||||
if ( cursor )
|
||||
@@ -132,9 +132,9 @@ module.exports = {
|
||||
.columns( [ { id: 'commits.id' }, 'message', 'referencedObject', 'sourceApplication', 'totalChildrenCount', 'parents', 'commits.createdAt', { branchName: 'branches.name' }, { authorName: 'users.name' }, { authorId: 'users.id' }, { authorAvatar: 'users.avatar' } ] )
|
||||
.select( )
|
||||
.join( 'commits', 'commits.id', 'stream_commits.commitId' )
|
||||
.join( 'users', 'commits.author', 'users.id' )
|
||||
.join( 'branch_commits', 'commits.id', 'branch_commits.commitId' )
|
||||
.join( 'branches', 'branches.id', 'branch_commits.branchId' )
|
||||
.leftJoin( 'users', 'commits.author', 'users.id' )
|
||||
.where( 'stream_commits.streamId', streamId )
|
||||
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ module.exports = {
|
||||
} )
|
||||
.where( knex.raw( 'object_children_closure."streamId" = ? AND parent = ?', [ streamId, objectId ] ) )
|
||||
.orderBy( 'objects.id' )
|
||||
return q.stream( )
|
||||
return q.stream( { highWaterMark: 2 } )
|
||||
},
|
||||
|
||||
async getObjectChildren( { streamId, objectId, limit, depth, select, cursor } ) {
|
||||
@@ -442,7 +442,7 @@ module.exports = {
|
||||
.andWhere( 'streamId', streamId )
|
||||
.orderBy( 'id' )
|
||||
.select( 'id', 'speckleType', 'totalChildrenCount', 'totalChildrenCountByDepth', 'createdAt', 'data' )
|
||||
return res.stream( )
|
||||
return res.stream( { highWaterMark: 2 } )
|
||||
},
|
||||
|
||||
async hasObjects( { streamId, objectIds } ) {
|
||||
|
||||
@@ -63,14 +63,16 @@ module.exports = {
|
||||
|
||||
async getUserById( { userId } ) {
|
||||
let user = await Users( ).where( { id: userId } ).select( '*' ).first( )
|
||||
delete user.passwordDigest
|
||||
if ( user )
|
||||
delete user.passwordDigest
|
||||
return user
|
||||
},
|
||||
|
||||
// TODO: deprecate
|
||||
async getUser( id ) {
|
||||
let user = await Users( ).where( { id: id } ).select( '*' ).first( )
|
||||
delete user.passwordDigest
|
||||
if ( user )
|
||||
delete user.passwordDigest
|
||||
return user
|
||||
},
|
||||
|
||||
@@ -150,8 +152,6 @@ module.exports = {
|
||||
for ( let i in streams.rows ) {
|
||||
await deleteStream( { streamId: streams.rows[i].id } )
|
||||
}
|
||||
await knex.raw( 'DELETE FROM commits WHERE author = ?', [ id ] )
|
||||
await knex.raw( 'DELETE FROM branches WHERE "authorId" = ?', [ id ] )
|
||||
|
||||
return await Users( ).where( { id: id } ).del( )
|
||||
}
|
||||
|
||||
@@ -15,6 +15,21 @@ const { createUser, findOrCreateUser, getUser, searchUsers, updateUser, deleteUs
|
||||
const { createPersonalAccessToken, createAppToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../services/tokens' )
|
||||
const { grantPermissionsStream, createStream, getStream } = require( '../services/streams' )
|
||||
|
||||
const {
|
||||
createBranch,
|
||||
getBranchesByStreamId
|
||||
} = require( '../services/branches' )
|
||||
|
||||
const {
|
||||
createCommitByBranchName,
|
||||
getCommitsByBranchName,
|
||||
getCommitById,
|
||||
getCommitsByStreamId,
|
||||
deleteCommit,
|
||||
} = require( '../services/commits' )
|
||||
|
||||
const { createObject, createObjects } = require( '../services/objects' )
|
||||
|
||||
describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
let myTestActor = {
|
||||
name: 'Dimitrie Stefanescu',
|
||||
@@ -110,29 +125,53 @@ describe( 'Actors & Tokens @user-services', ( ) => {
|
||||
|
||||
} )
|
||||
|
||||
// Note: deletion is more complicated.
|
||||
it( 'Should delete a user', async ( ) => {
|
||||
let soloOwnerStream = { name: 'Test Stream 01', description: 'wonderful test stream', isPublic: true }
|
||||
let multiOwnerStream = { name: 'Test Stream 02', description: 'another test stream', isPublic: true }
|
||||
|
||||
soloOwnerStream.id = await createStream( { ...soloOwnerStream, ownerId: ballmerUserId } )
|
||||
multiOwnerStream.id = await createStream( { ...multiOwnerStream, ownerId: ballmerUserId } )
|
||||
|
||||
await grantPermissionsStream( { streamId: multiOwnerStream.id, userId: myTestActor.id, role: 'stream:owner' } )
|
||||
|
||||
// create a branch for ballmer on the multiowner stream
|
||||
let branch = { name: 'ballmer/dev' }
|
||||
branch.id = await createBranch( { ...branch, streamId: multiOwnerStream.id, authorId: ballmerUserId } )
|
||||
|
||||
let branchSecond = { name: 'steve/jobs' }
|
||||
branchSecond.id = await createBranch( { ...branchSecond, streamId: multiOwnerStream.id, authorId: myTestActor.id } )
|
||||
|
||||
// create an object and a commit around it on the multiowner stream
|
||||
let objId = await createObject( multiOwnerStream.id, { pie: 'in the sky' } )
|
||||
let commitId = await createCommitByBranchName( { streamId: multiOwnerStream.id, branchName: 'ballmer/dev', message: 'breakfast commit', sourceApplication: 'tests', objectId:objId, authorId: ballmerUserId } )
|
||||
|
||||
await deleteUser( ballmerUserId )
|
||||
|
||||
if ( await getStream( { streamId: soloOwnerStream.id } ) !== undefined ) {
|
||||
assert.fail( 'user stream not deleted' )
|
||||
}
|
||||
|
||||
let multiOwnerStreamCopy = await getStream( { streamId: multiOwnerStream.id } )
|
||||
if ( !multiOwnerStreamCopy || multiOwnerStreamCopy.id != multiOwnerStream.id ) {
|
||||
assert.fail( 'shared stream deleted' )
|
||||
}
|
||||
|
||||
try {
|
||||
let user = await getUser( ballmerUserId )
|
||||
} catch ( e ) {
|
||||
return
|
||||
}
|
||||
assert.fail( 'user not deleted' )
|
||||
let branches = await getBranchesByStreamId( { streamId: multiOwnerStream.id } )
|
||||
expect( branches.items.length ).to.equal( 3 )
|
||||
|
||||
let branchCommits = await getCommitsByBranchName( { streamId: multiOwnerStream.id, branchName:'ballmer/dev' } )
|
||||
expect( branchCommits.commits.length ).to.equal( 1 )
|
||||
|
||||
let commit = await getCommitById( { id: commitId } )
|
||||
expect( commit ).to.be.not.null
|
||||
|
||||
let commitsByStreamId = await getCommitsByStreamId( { streamId: multiOwnerStream.id } )
|
||||
expect( commitsByStreamId.commits.length ).to.equal( 1 )
|
||||
|
||||
let user = await getUser( ballmerUserId )
|
||||
if ( user )
|
||||
assert.fail( 'user not deleted' )
|
||||
} )
|
||||
|
||||
it( 'Should get a user', async ( ) => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const appRoot = require( 'app-root-path' )
|
||||
const crs = require( 'crypto-random-string' )
|
||||
const knex = require( `${appRoot}/db/knex` )
|
||||
const sanitizeHtml = require( 'sanitize-html' )
|
||||
|
||||
const { getUserByEmail, getUserById } = require( `${appRoot}/modules/core/services/users` )
|
||||
|
||||
@@ -19,6 +20,15 @@ module.exports = {
|
||||
|
||||
if ( existingUser ) throw new Error( 'This email is already associated with an account on this server!' )
|
||||
|
||||
if ( message ) {
|
||||
|
||||
if ( message.length >= 1024 ) {
|
||||
throw new Error( 'Personal message too long.' )
|
||||
}
|
||||
|
||||
message = module.exports.sanitizeMessage( message )
|
||||
}
|
||||
|
||||
// check if email is already invited
|
||||
let existingInvite = await module.exports.getInviteByEmail( { email } )
|
||||
if ( existingInvite ) throw new Error( 'Already invited!' )
|
||||
@@ -126,5 +136,11 @@ This email was sent from ${serverInfo.name} at ${process.env.CANONICAL_URL}, dep
|
||||
|
||||
await Invites().where( { id: id } ).update( { used: true } )
|
||||
return true
|
||||
},
|
||||
|
||||
async sanitizeMessage( message ) {
|
||||
return sanitizeHtml( message, {
|
||||
allowedTags: [ 'b', 'i', 'em', 'strong' ],
|
||||
} )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const chai = require( 'chai' )
|
||||
const request = require( 'supertest' )
|
||||
const assert = require( 'assert' )
|
||||
const appRoot = require( 'app-root-path' )
|
||||
const { async } = require( 'crypto-random-string' )
|
||||
const { init, startHttp } = require( `${appRoot}/app` )
|
||||
|
||||
const expect = chai.expect
|
||||
@@ -11,9 +12,8 @@ const expect = chai.expect
|
||||
const knex = require( `${appRoot}/db/knex` )
|
||||
const { createUser } = require( `${appRoot}/modules/core/services/users` )
|
||||
|
||||
const { createAndSendInvite, getInviteById, getInviteByEmail, validateInvite, useInvite } = require( `${appRoot}/modules/serverinvites/services` )
|
||||
const { createAndSendInvite, getInviteById, getInviteByEmail, validateInvite, useInvite, sanitizeMessage } = require( `${appRoot}/modules/serverinvites/services` )
|
||||
const { createStream, getStream, getStreamUsers, getUserStreams } = require( `${appRoot}/modules/core/services/streams` )
|
||||
const { createPersonalAccessToken } = require( `${appRoot}/modules/core/services/tokens` )
|
||||
|
||||
const serverAddress = `http://localhost:${process.env.PORT || 3000}`
|
||||
|
||||
@@ -55,30 +55,51 @@ describe( 'Server Invites @server-invites', ( ) => {
|
||||
|
||||
try {
|
||||
await createAndSendInvite( { email:'cat@speckle.systems', inviterId: actor.id, message: 'Hey, join!' } )
|
||||
assert.fail()
|
||||
} catch ( e ) {
|
||||
// pass
|
||||
return
|
||||
}
|
||||
assert.fail( 'should not allow multiple invites for the same email' )
|
||||
} )
|
||||
|
||||
it( 'should not allow self invites', async() => {
|
||||
|
||||
try {
|
||||
await createAndSendInvite( { email: 'didimitrie-100@gmail.com', inviterId: actor.id } )
|
||||
assert.fail()
|
||||
} catch ( e ) {
|
||||
// pass
|
||||
return
|
||||
}
|
||||
assert.fail( 'should not allow self invites' )
|
||||
} )
|
||||
|
||||
it( 'should not allow invites from no user', async() => {
|
||||
|
||||
try {
|
||||
await createAndSendInvite( { email: 'didimitrie233-100@gmail.com', inviterId: 'fake' } )
|
||||
assert.fail()
|
||||
} catch ( e ) {
|
||||
// pass
|
||||
return
|
||||
}
|
||||
assert.fail( 'should not allow invites from no user' )
|
||||
} )
|
||||
|
||||
it( 'should not allow invites with a too long message', async() => {
|
||||
|
||||
try {
|
||||
let inviteId = await createAndSendInvite( {
|
||||
email: '123456@gmail.com',
|
||||
inviterId: actor.id,
|
||||
message: longInviteMessage
|
||||
} )
|
||||
} catch ( e ){
|
||||
return
|
||||
}
|
||||
|
||||
assert.fail( 'created invite with too long message' )
|
||||
} )
|
||||
|
||||
it( 'should sanitize invite messages', async() => {
|
||||
let clean = await sanitizeMessage( 'Click on my <b><a href="https://spam.com">spam link please</a></b>!' )
|
||||
const includesLink = clean.includes( '<a' )
|
||||
expect( includesLink ).to.be.false
|
||||
} )
|
||||
|
||||
it( 'should get an invite by id', async() => {
|
||||
@@ -220,3 +241,6 @@ function sendRequest( auth, obj, address = serverAddress ) {
|
||||
return chai.request( address ).post( '/graphql' ).set( 'Authorization', auth ).send( obj )
|
||||
|
||||
}
|
||||
|
||||
const longInviteMessage =
|
||||
'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.'
|
||||
|
||||
Generated
+117
@@ -4498,6 +4498,11 @@
|
||||
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
|
||||
"dev": true
|
||||
},
|
||||
"deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||
},
|
||||
"default-require-extensions": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz",
|
||||
@@ -4666,6 +4671,39 @@
|
||||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
|
||||
"integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
|
||||
"requires": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.2.0",
|
||||
"entities": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"domelementtype": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
|
||||
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A=="
|
||||
},
|
||||
"domhandler": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz",
|
||||
"integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==",
|
||||
"requires": {
|
||||
"domelementtype": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"domutils": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz",
|
||||
"integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==",
|
||||
"requires": {
|
||||
"dom-serializer": "^1.0.1",
|
||||
"domelementtype": "^2.2.0",
|
||||
"domhandler": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"dot-prop": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz",
|
||||
@@ -4798,6 +4836,11 @@
|
||||
"ansi-colors": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
|
||||
},
|
||||
"env-paths": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz",
|
||||
@@ -6794,6 +6837,17 @@
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true
|
||||
},
|
||||
"htmlparser2": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
||||
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
|
||||
"requires": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.0.0",
|
||||
"domutils": "^2.5.2",
|
||||
"entities": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"http-cache-semantics": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz",
|
||||
@@ -7834,6 +7888,11 @@
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
||||
},
|
||||
"klona": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
|
||||
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA=="
|
||||
},
|
||||
"knex": {
|
||||
"version": "0.21.15",
|
||||
"resolved": "https://registry.npmjs.org/knex/-/knex-0.21.15.tgz",
|
||||
@@ -8830,6 +8889,11 @@
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
|
||||
"optional": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.1.23",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
|
||||
"integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw=="
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
||||
@@ -9776,6 +9840,11 @@
|
||||
"protocols": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"parse-srcset": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||
"integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE="
|
||||
},
|
||||
"parse-url": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.2.tgz",
|
||||
@@ -10073,6 +10142,23 @@
|
||||
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
|
||||
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.0.tgz",
|
||||
"integrity": "sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==",
|
||||
"requires": {
|
||||
"colorette": "^1.2.2",
|
||||
"nanoid": "^3.1.23",
|
||||
"source-map-js": "^0.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"colorette": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
@@ -10863,6 +10949,32 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sanitize-html": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.4.0.tgz",
|
||||
"integrity": "sha512-Y1OgkUiTPMqwZNRLPERSEi39iOebn2XJLbeiGOBhaJD/yLqtLGu6GE5w7evx177LeGgSE+4p4e107LMiydOf6A==",
|
||||
"requires": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"htmlparser2": "^6.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"klona": "^2.0.3",
|
||||
"parse-srcset": "^1.0.2",
|
||||
"postcss": "^8.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||
},
|
||||
"is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
@@ -11245,6 +11357,11 @@
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz",
|
||||
"integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug=="
|
||||
},
|
||||
"source-map-resolve": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"pg-query-stream": "^3.4.2",
|
||||
"prom-client": "^13.1.0",
|
||||
"redis": "^3.1.1",
|
||||
"sanitize-html": "^2.4.0",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
-6
@@ -2008,12 +2008,6 @@
|
||||
"@sinonjs/commons": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"@speckle/objectloader": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/objectloader/-/objectloader-2.0.3.tgz",
|
||||
"integrity": "sha512-hSyJU0ktZOYbgjDtwrHHXowRu5L0lxoO32N2JPJUjURoy+M1ZpvJVGyT4jKG03HvH30j9rynja0PVjVoW9LdEw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/anymatch": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
|
||||
|
||||
@@ -91,6 +91,7 @@ export default class Coverter {
|
||||
// Last attempt: iterate through all object keys and see if we can display anything!
|
||||
// traverses the object in case there's any sub-objects we can convert.
|
||||
for ( let prop in target ) {
|
||||
if ( prop === 'bbox' ) continue
|
||||
if ( typeof target[prop] !== 'object' ) continue
|
||||
let childPromise = this.traverseAndConvert( target[prop], callback, scale )
|
||||
childrenConversionPromisses.push( childPromise )
|
||||
@@ -363,6 +364,20 @@ export default class Coverter {
|
||||
return new ObjectWrapper( geometry, obj, 'line' )
|
||||
}
|
||||
|
||||
async BoxToBufferGeometry( object, scale = true ){
|
||||
let conversionFactor = scale ? getConversionFactor( object.units ) : 1
|
||||
|
||||
var move = this.PointToVector3( object.basePlane.origin )
|
||||
var width = ( object.xSize.end - object.xSize.start ) * conversionFactor
|
||||
var depth = ( object.ySize.end - object.ySize.start ) * conversionFactor
|
||||
var height = ( object.zSize.end - object.zSize.start ) * conversionFactor
|
||||
|
||||
var box = new THREE.BoxBufferGeometry( width,height,depth,1,1,1 )
|
||||
box.applyMatrix4( new THREE.Matrix4().setPosition( move ) )
|
||||
|
||||
return new ObjectWrapper( box, object )
|
||||
}
|
||||
|
||||
async PolycurveToBufferGeometry( object, scale = true ) {
|
||||
let obj = {}
|
||||
Object.assign( obj, object )
|
||||
|
||||
Reference in New Issue
Block a user