+1
-6
@@ -7,12 +7,7 @@
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 11
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"modules/*/tests/*",
|
||||
"node_modules/*",
|
||||
"frontend/*",
|
||||
"*.graphql"
|
||||
],
|
||||
"ignorePatterns": ["modules/*/tests/*", "node_modules/*"],
|
||||
"rules": {
|
||||
"arrow-spacing": [
|
||||
2,
|
||||
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"vetur.format.defaultFormatterOptions": {
|
||||
"js-beautify-html": {
|
||||
"wrap_attributes": "force-expand-multiline"
|
||||
},
|
||||
"prettyhtml": {
|
||||
"printWidth": 100,
|
||||
"singleQuote": false,
|
||||
"wrapAttributes": false,
|
||||
"sortAttributes": false
|
||||
}
|
||||
},
|
||||
|
||||
"editor.formatOnSave": true,
|
||||
"vetur.validation.template": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"cSpell.userWords": ["vetur", "vuetify"],
|
||||
"vetur.format.defaultFormatter.js": "none",
|
||||
"vetur.format.defaultFormatter.ts": "none",
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"eslint.validate": ["vue"]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#########################################################vvv
|
||||
# Local dev environments
|
||||
# If your frontend is served in dev from somewhere else,
|
||||
# this is going to help out :)
|
||||
#########################################################vvv
|
||||
# PORT=8081
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"ignorePatterns": ["main-frontend.js"],
|
||||
"extends": [
|
||||
"plugin:vue/recommended",
|
||||
"prettier",
|
||||
"prettier/vue",
|
||||
"plugin:prettier/recommended",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["vue", "prettier"],
|
||||
"rules": {
|
||||
"arrow-spacing": [
|
||||
2,
|
||||
{
|
||||
"before": true,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
//"array-bracket-spacing": [2, "always"],
|
||||
"block-spacing": [2, "always"],
|
||||
"camelcase": [
|
||||
1,
|
||||
{
|
||||
"properties": "always"
|
||||
}
|
||||
],
|
||||
// "space-in-parens": [2, "always"],
|
||||
"keyword-spacing": 2,
|
||||
"semi": "off",
|
||||
"indent": ["error", 2],
|
||||
"space-unary-ops": [
|
||||
2,
|
||||
{
|
||||
"words": true,
|
||||
"nonwords": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"endOfLine": "auto",
|
||||
"bracketSpacing": true,
|
||||
"eslintIntegration": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
const path = require('path')
|
||||
const path = require( 'path' )
|
||||
|
||||
// Load .env files
|
||||
const { loadEnv } = require('vue-cli-plugin-apollo/utils/load-env')
|
||||
const env = loadEnv([
|
||||
path.resolve(__dirname, '.env'),
|
||||
path.resolve(__dirname, '.env.local')
|
||||
])
|
||||
const { loadEnv } = require( 'vue-cli-plugin-apollo/utils/load-env' )
|
||||
const env = loadEnv( [
|
||||
path.resolve( __dirname, '.env' ),
|
||||
path.resolve( __dirname, '.env.local' )
|
||||
] )
|
||||
|
||||
module.exports = {
|
||||
client: {
|
||||
service: env.VUE_APP_APOLLO_ENGINE_SERVICE,
|
||||
includes: ['src/**/*.{js,jsx,ts,tsx,vue,gql}']
|
||||
includes: [ 'src/**/*.{js,jsx,ts,tsx,vue,gql}' ]
|
||||
},
|
||||
service: {
|
||||
name: env.VUE_APP_APOLLO_ENGINE_SERVICE,
|
||||
localSchemaFile: path.resolve(__dirname, './node_modules/.temp/graphql/schema.json')
|
||||
localSchemaFile: path.resolve( __dirname, './node_modules/.temp/graphql/schema.json' )
|
||||
},
|
||||
engine: {
|
||||
endpoint: process.env.APOLLO_ENGINE_API_ENDPOINT,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
presets: ["@vue/cli-plugin-babel/preset"]
|
||||
}
|
||||
|
||||
Generated
+929
-52
File diff suppressed because it is too large
Load Diff
+22
-12
@@ -10,26 +10,36 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.4",
|
||||
"crypto-random-string": "^3.2.0",
|
||||
"crypto-random-string": "^3.3.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"vue": "^2.6.11",
|
||||
"vue-apollo": "^3.0.0-beta.11",
|
||||
"vue-router": "^3.1.6",
|
||||
"vuetify": "^2.2.11",
|
||||
"vuex": "^3.1.3"
|
||||
"v-tooltip": "^2.0.3",
|
||||
"vue": "^2.6.12",
|
||||
"vue-apollo": "^3.0.4",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-router": "^3.4.5",
|
||||
"vue-timeago": "^5.1.2",
|
||||
"vuetify": "^2.3.12",
|
||||
"vuex": "^3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdi/font": "^5.3.45",
|
||||
"@mdi/font": "^5.6.55",
|
||||
"@vue/cli-plugin-babel": "~4.3.0",
|
||||
"@vue/cli-plugin-router": "~4.3.0",
|
||||
"@vue/cli-plugin-vuex": "~4.3.0",
|
||||
"@vue/cli-service": "~4.3.0",
|
||||
"graphql-tag": "^2.9.0",
|
||||
"sass": "^1.19.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.10.0",
|
||||
"eslint-config-prettier": "^6.12.0",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-vue": "^7.0.0",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"prettier": "^2.1.2",
|
||||
"sass": "^1.26.11",
|
||||
"sass-loader": "^8.0.0",
|
||||
"vue-cli-plugin-apollo": "~0.21.3",
|
||||
"vue-cli-plugin-vuetify": "~2.0.5",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuetify-loader": "^1.3.0"
|
||||
"vue-cli-plugin-vuetify": "^2.0.7",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vuetify-loader": "^1.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 15 KiB |
+162
-99
@@ -1,62 +1,107 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-container fluid fill-height v-if='!error'>
|
||||
<v-row align='center' justify='center'>
|
||||
<v-col xs='10' sm='6' md='5' lg='4' xl='3' class=''>
|
||||
Err: {{error}} : {{errorMessage}} // Local: {{hasLocalStrategy}}
|
||||
<v-card class='elevation-20'>
|
||||
<v-img class="white--text align-end" height="100px" src="./assets/s2logo-wide.svg"></v-img>
|
||||
<v-card-text class='pa-1'>
|
||||
<v-container v-if="!error" fluid fill-height>
|
||||
<v-row align="center" justify="center">
|
||||
<v-col xs="10" sm="6" md="5" lg="4" xl="3" class="">
|
||||
Err: {{ error }} : {{ errorMessage }} // Local: {{ hasLocalStrategy }}
|
||||
<v-card class="elevation-20">
|
||||
<v-img
|
||||
class="white--text align-end"
|
||||
height="100px"
|
||||
src="./assets/s2logo-wide.svg"
|
||||
></v-img>
|
||||
<v-card-text class="pa-1">
|
||||
<v-container fluid>
|
||||
<v-row style='margin-top:-10px;' dense>
|
||||
<v-col cols=12>
|
||||
<p class='title font-weight-light text-center' v-if='app.firstparty'>
|
||||
Signing in to <b>{{app.name}}</b>
|
||||
<v-tooltip bottom v-if='app.firstparty'>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon color='accent' style='margin-top:-6px' v-on="on">mdi-shield-check</v-icon>
|
||||
<v-row style="margin-top: -10px" dense>
|
||||
<v-col cols="12">
|
||||
<p
|
||||
v-if="app.firstparty"
|
||||
class="title font-weight-light text-center"
|
||||
>
|
||||
Signing in to
|
||||
<b>{{ app.name }}</b>
|
||||
|
||||
<v-tooltip v-if="app.firstparty" bottom>
|
||||
<template #activator="{ on }">
|
||||
<v-icon
|
||||
color="primary"
|
||||
style="margin-top: -6px"
|
||||
v-on="on"
|
||||
>
|
||||
mdi-shield-check
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>Verified application.</span>
|
||||
</v-tooltip>
|
||||
</p>
|
||||
<p class='title font-weight-light text-center' v-if='!app.firstparty && !isFinalizing'>
|
||||
You need to sign in first<br>to authorize <span class='accent--text'><b>{{app.name}}</b></span> by <b>{{app.author}}.</b>
|
||||
<p
|
||||
v-if="!app.firstparty && !isFinalizing"
|
||||
class="title font-weight-light text-center"
|
||||
>
|
||||
You need to sign in first
|
||||
<br />
|
||||
to authorize
|
||||
<span class="primary--text">
|
||||
<b>{{ app.name }}</b>
|
||||
</span>
|
||||
by
|
||||
<b>{{ app.author }}.</b>
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<router-view></router-view>
|
||||
<v-container v-if='!isFinalizing'>
|
||||
<v-container v-if="!isFinalizing">
|
||||
<v-row>
|
||||
<template v-for='s in strategies'>
|
||||
<v-col cols='12' class='text-center py-0 my-0'>
|
||||
<v-btn block large tile :color='s.color' dark :key='s.name' class='my-2' :href='`${s.url}?appId=${appId}&challenge=${challenge}${ suuid ? "&suuid=" + suuid : "" }`'>{{s.name}}</v-btn>
|
||||
<template v-for="s in strategies">
|
||||
<v-col cols="12" class="text-center py-0 my-0">
|
||||
<v-btn
|
||||
:key="s.name"
|
||||
block
|
||||
large
|
||||
tile
|
||||
:color="s.color"
|
||||
dark
|
||||
class="my-2"
|
||||
:href="`${s.url}?appId=${appId}&challenge=${challenge}${
|
||||
suuid ? '&suuid=' + suuid : ''
|
||||
}`"
|
||||
>
|
||||
{{ s.name }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text class='blue-grey lighten-5'>
|
||||
<div class='text-center'>
|
||||
<b>{{serverInfo.name}}</b> <br>deployed by<br><b>{{serverInfo.company}}</b>
|
||||
<br>
|
||||
<v-divider class='my-2'></v-divider>
|
||||
<b>Terms of Service:</b> {{serverInfo.termsOfService}}
|
||||
<br>
|
||||
<b>Support:</b> {{serverInfo.adminContact}}
|
||||
<v-card-text class="blue-grey lighten-5">
|
||||
<div class="text-center">
|
||||
<b>{{ serverInfo.name }}</b>
|
||||
<br />
|
||||
deployed by
|
||||
<br />
|
||||
<b>{{ serverInfo.company }}</b>
|
||||
<br />
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<b>Terms of Service:</b>
|
||||
{{ serverInfo.termsOfService }}
|
||||
<br />
|
||||
<b>Support:</b>
|
||||
{{ serverInfo.adminContact }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-container fluid fill-height v-else>
|
||||
<v-row align='center' justify='center'>
|
||||
<v-col xs='10' sm='6' md='5' lg='4' xl='3' class=''>
|
||||
<v-card class='elevation-20' color='red'>
|
||||
<v-card-text class='white--text title'>
|
||||
<v-icon color='white'>mdi-bug</v-icon> {{errorMessage}}
|
||||
<v-container v-else fluid fill-height>
|
||||
<v-row align="center" justify="center">
|
||||
<v-col xs="10" sm="6" md="5" lg="4" xl="3" class="">
|
||||
<v-card class="elevation-20" color="red">
|
||||
<v-card-text class="white--text title">
|
||||
<v-icon color="white">mdi-bug</v-icon>
|
||||
{{ errorMessage }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@@ -65,54 +110,62 @@
|
||||
</v-app>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import debounce from 'lodash.debounce'
|
||||
import crs from 'crypto-random-string'
|
||||
import gql from "graphql-tag"
|
||||
import debounce from "lodash.debounce"
|
||||
import crs from "crypto-random-string"
|
||||
export default {
|
||||
name: 'AppAuth',
|
||||
name: "AppAuth",
|
||||
apollo: {
|
||||
serverInfo: {
|
||||
query: gql ` query { serverInfo { name company adminContact termsOfService scopes { name description } authStrategies { id name color icon url } } } `,
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
adminContact
|
||||
termsOfService
|
||||
scopes {
|
||||
name
|
||||
description
|
||||
}
|
||||
authStrategies {
|
||||
id
|
||||
name
|
||||
color
|
||||
icon
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
},
|
||||
app: {
|
||||
query( ) {
|
||||
return gql ` query { app( id: "${this.appId}") { id name redirectUrl scopes {name description} } } `
|
||||
query() {
|
||||
return gql` query { app( id: "${this.appId}") { id name redirectUrl scopes {name description} } } `
|
||||
},
|
||||
skip( ) {
|
||||
skip() {
|
||||
return this.appId === null
|
||||
},
|
||||
data( {
|
||||
data
|
||||
}, key ) {
|
||||
if ( data.errors ) {
|
||||
data({ data }, key) {
|
||||
if (data.errors) {
|
||||
this.error = true
|
||||
this.errorMessage = 'Invalid app authorization request: app not registered on this server.'
|
||||
console.log( 'Error: No such application!!!!!' )
|
||||
this.errorMessage =
|
||||
"Invalid app authorization request: app not registered on this server."
|
||||
console.log("Error: No such application!!!!!")
|
||||
}
|
||||
},
|
||||
error( err ) {
|
||||
error(err) {
|
||||
this.error = true
|
||||
this.errorMessage = `Invalid app authorization request: could not find app with id "${this.appId}" on this server.`
|
||||
console.log( 'Error: No such application' )
|
||||
console.log("Error: No such application")
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isFinalizing( ) {
|
||||
return this.$route.path.indexOf( '/finalize' ) !== -1
|
||||
},
|
||||
hasLocalStrategy( ) {
|
||||
return this.serverInfo.authStrategies.findIndex( s => s.id === 'local' ) !== -1
|
||||
},
|
||||
strategies( ) {
|
||||
return this.serverInfo.authStrategies.filter( s => s.id !== 'local' )
|
||||
}
|
||||
},
|
||||
components: {},
|
||||
data: ( ) => ( {
|
||||
data: () => ({
|
||||
serverInfo: {
|
||||
name: 'Loading',
|
||||
authStrategies: [ ]
|
||||
name: "Loading",
|
||||
authStrategies: []
|
||||
},
|
||||
appId: null,
|
||||
challenge: null,
|
||||
@@ -121,7 +174,7 @@ export default {
|
||||
name: null,
|
||||
author: null,
|
||||
firstparty: null,
|
||||
scopes: [ ]
|
||||
scopes: []
|
||||
},
|
||||
loggedIn: null,
|
||||
profile: {
|
||||
@@ -132,58 +185,68 @@ export default {
|
||||
},
|
||||
error: false,
|
||||
errorMessage: null
|
||||
} ),
|
||||
methods: {
|
||||
goToStrategy( ) {}
|
||||
}),
|
||||
computed: {
|
||||
isFinalizing() {
|
||||
return this.$route.path.indexOf("/finalize") !== -1
|
||||
},
|
||||
hasLocalStrategy() {
|
||||
return (
|
||||
this.serverInfo.authStrategies.findIndex((s) => s.id === "local") !== -1
|
||||
)
|
||||
},
|
||||
strategies() {
|
||||
return this.serverInfo.authStrategies.filter((s) => s.id !== "local")
|
||||
}
|
||||
},
|
||||
mounted( ) {
|
||||
let urlParams = new URLSearchParams( window.location.search )
|
||||
let appId = urlParams.get( 'appId' )
|
||||
let challenge = urlParams.get( 'challenge' )
|
||||
let suuid = urlParams.get( 'suuid' )
|
||||
mounted() {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
let appId = urlParams.get("appId")
|
||||
let challenge = urlParams.get("challenge")
|
||||
let suuid = urlParams.get("suuid")
|
||||
this.suuid = suuid
|
||||
|
||||
if ( !appId )
|
||||
this.appId = 'spklwebapp'
|
||||
else
|
||||
this.appId = appId
|
||||
if ( !challenge && this.appId === 'spklwebapp' ) {
|
||||
if ( localStorage.getItem( 'appChallenge' ) ) {
|
||||
if (!appId) this.appId = "spklwebapp"
|
||||
else this.appId = appId
|
||||
if (!challenge && this.appId === "spklwebapp") {
|
||||
if (localStorage.getItem("appChallenge")) {
|
||||
// Do nothing!
|
||||
} else {
|
||||
this.challenge = crs( {
|
||||
this.challenge = crs({
|
||||
length: 10
|
||||
} )
|
||||
localStorage.setItem( 'appChallenge', this.challenge )
|
||||
})
|
||||
localStorage.setItem("appChallenge", this.challenge)
|
||||
}
|
||||
} else if ( challenge ) {
|
||||
} else if (challenge) {
|
||||
this.challenge = challenge
|
||||
} else {
|
||||
if ( window.location.href.indexOf( '/finalize' ) === -1 ) {
|
||||
if (window.location.href.indexOf("/finalize") === -1) {
|
||||
this.error = true
|
||||
this.errorMessage = 'Invalid app authorization request: missing challenge.'
|
||||
this.errorMessage =
|
||||
"Invalid app authorization request: missing challenge."
|
||||
}
|
||||
}
|
||||
},
|
||||
async beforeCreate( ) {
|
||||
async beforeCreate() {
|
||||
// checks login
|
||||
let token = localStorage.getItem( 'AuthToken' )
|
||||
if ( token ) {
|
||||
let testResponse = await fetch( '/graphql', {
|
||||
method: 'POST',
|
||||
let token = localStorage.getItem("AuthToken")
|
||||
if (token) {
|
||||
let testResponse = await fetch("/graphql", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
Authorization: "Bearer " + token,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
body: JSON.stringify({
|
||||
query: `{ user { id } }`
|
||||
} )
|
||||
} )
|
||||
let data = ( await testResponse.json( ) ).data
|
||||
if ( data.user )
|
||||
return true
|
||||
})
|
||||
})
|
||||
let data = (await testResponse.json()).data
|
||||
if (data.user) return true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToStrategy() {}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
+350
-22
@@ -1,33 +1,361 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-app-bar app clipped-left color="primary" dark>
|
||||
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
|
||||
<span class="title ml-3 mr-5">Speckle <span class="font-weight-light">2</span></span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-app id="speckle">
|
||||
<v-app-bar app color="background2" flat class="no-decor">
|
||||
<v-container class="py-0 fill-height">
|
||||
<v-btn text to="/" active-class="no-active">
|
||||
<v-img
|
||||
contain
|
||||
max-height="30"
|
||||
max-width="30"
|
||||
src="./assets/logo.svg"
|
||||
/>
|
||||
<div class="mt-1">
|
||||
<span class="primary--text"><b>SPECKLE</b></span>
|
||||
<!--
|
||||
<span class="font-weight-light">ADMIN</span> -->
|
||||
</div>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-for="link in navLinks"
|
||||
:key="link.name"
|
||||
text
|
||||
class="text-uppercase"
|
||||
:to="link.link"
|
||||
>
|
||||
{{ link.name }}
|
||||
</v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-responsive max-width="300">
|
||||
<v-autocomplete
|
||||
v-model="selectedSearchResult"
|
||||
:loading="$apollo.loading"
|
||||
:items="streams.items"
|
||||
:search-input.sync="search"
|
||||
no-filter
|
||||
counter="3"
|
||||
rounded
|
||||
filled
|
||||
dense
|
||||
flat
|
||||
hide-no-data
|
||||
hide-details
|
||||
placeholder="Search streams..."
|
||||
item-text="name"
|
||||
item-value="id"
|
||||
return-object
|
||||
clearable
|
||||
append-icon=""
|
||||
>
|
||||
<template #item="{ item }" color="background">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<v-row class="pa-0 ma-0">
|
||||
{{ item.name }}
|
||||
<v-spacer></v-spacer>
|
||||
<span class="streamid">{{ item.id }}</span>
|
||||
</v-row>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
v-text="item.description"
|
||||
></v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="caption">
|
||||
Updated
|
||||
<timeago :datetime="item.updatedAt"></timeago>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</v-responsive>
|
||||
<v-menu v-if="user" bottom left offset-y class="userMenu">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
icon
|
||||
v-bind="attrs"
|
||||
height="38"
|
||||
width="38"
|
||||
class="ml-3"
|
||||
v-on="on"
|
||||
>
|
||||
<v-avatar color="background" size="38">
|
||||
<v-img v-if="user.avatar" :src="user.avatar" />
|
||||
<v-img
|
||||
v-else
|
||||
:src="`https://robohash.org/` + user.id + `.png?size=38x38`"
|
||||
/>
|
||||
</v-avatar>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense class="userMenu" color="background2">
|
||||
<v-list-item>
|
||||
<v-list-item-content class="caption">
|
||||
Signed in as:
|
||||
<strong>{{ user.name }}</strong>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item
|
||||
v-if="!this.$vuetify.theme.dark"
|
||||
link
|
||||
@click="switchTheme"
|
||||
>
|
||||
<v-list-item-content>Dark mode</v-list-item-content>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-weather-night</v-icon>
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
<v-list-item v-else exact @click="switchTheme">
|
||||
<v-list-item-content>Light mode</v-list-item-content>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-white-balance-sunny</v-icon>
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
<!-- <v-list-item href="https://speckle.systems/" target="_blank">
|
||||
<v-list-item-content>SpeckleSystems</v-list-item-content>
|
||||
</v-list-item> -->
|
||||
<v-list-item @click="signOut">
|
||||
<v-list-item-content>Sign out</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-container>
|
||||
</v-app-bar>
|
||||
<v-navigation-drawer v-model="drawer" app clipped color="grey lighten-4">
|
||||
So you think somthig should be here?
|
||||
</v-navigation-drawer>
|
||||
<v-content>
|
||||
|
||||
<v-main :style="background">
|
||||
<router-view></router-view>
|
||||
</v-content>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
<script>
|
||||
import HelloWorld from './components/HelloWorld';
|
||||
import userQuery from "./graphql/user.gql"
|
||||
import gql from "graphql-tag"
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
||||
components: {
|
||||
HelloWorld,
|
||||
data: () => ({
|
||||
search: "",
|
||||
streams: { items: [] },
|
||||
selectedSearchResult: null,
|
||||
navLinks: [
|
||||
{ link: "/streams", name: "streams" },
|
||||
{ link: "/help", name: "help" }
|
||||
]
|
||||
}),
|
||||
apollo: {
|
||||
user: {
|
||||
prefetch: true,
|
||||
query: userQuery
|
||||
},
|
||||
streams: {
|
||||
query: gql`
|
||||
query Streams($query: String) {
|
||||
streams(query: $query) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
name
|
||||
description
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
query: this.search
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return !this.search || this.search.length < 3
|
||||
},
|
||||
debounce: 300
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
background() {
|
||||
let theme = this.$vuetify.theme.dark ? "dark" : "light"
|
||||
return `background-color: ${this.$vuetify.theme.themes[theme].background};`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedSearchResult(val) {
|
||||
this.search = ""
|
||||
this.streams.items = []
|
||||
if (val)
|
||||
this.$router.push({ name: "stream", params: { streamId: val.id } })
|
||||
},
|
||||
"streams.items"(val) {
|
||||
console.log(val)
|
||||
}
|
||||
},
|
||||
|
||||
data: ( ) => ( {
|
||||
setup: true,
|
||||
drawer: true
|
||||
} ),
|
||||
|
||||
|
||||
methods: {
|
||||
switchTheme() {
|
||||
this.$vuetify.theme.dark = !this.$vuetify.theme.dark
|
||||
localStorage.setItem(
|
||||
"darkModeEnabled",
|
||||
this.$vuetify.theme.dark ? "dark" : "light"
|
||||
)
|
||||
},
|
||||
signOut() {
|
||||
localStorage.clear()
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
<style>
|
||||
.v-card__text,
|
||||
.v-card__title {
|
||||
word-break: normal !important;
|
||||
}
|
||||
|
||||
.streamid {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.no-decor a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.v-btn--active.no-active::before {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
/* .theme--dark {
|
||||
/color: #cfcdcc !important;
|
||||
} */
|
||||
|
||||
/* don't like fat text */
|
||||
.v-list-item--dense .v-list-item__title,
|
||||
.v-list-item--dense .v-list-item__subtitle,
|
||||
.v-list--dense .v-list-item .v-list-item__title,
|
||||
.v-list--dense .v-list-item .v-list-item__subtitle {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
/* DARK MODE HARD FIXES */
|
||||
|
||||
.theme--dark.v-list {
|
||||
background-color: #303132 !important;
|
||||
}
|
||||
|
||||
/* TOOLTIPs */
|
||||
|
||||
.tooltip {
|
||||
display: block !important;
|
||||
z-index: 10000;
|
||||
font-family: "Roboto", sans-serif !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-inner {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
padding: 5px 10px 4px;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
margin: 5px;
|
||||
border-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="top"] {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="top"] .tooltip-arrow {
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
bottom: -5px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="bottom"] {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="bottom"] .tooltip-arrow {
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
top: -5px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="right"] {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="right"] .tooltip-arrow {
|
||||
border-width: 5px 5px 5px 0;
|
||||
border-left-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
left: -5px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="left"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="left"] .tooltip-arrow {
|
||||
border-width: 5px 0 5px 5px;
|
||||
border-top-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
right: -5px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tooltip.popover .popover-inner {
|
||||
background: #f9f9f9;
|
||||
color: black;
|
||||
padding: 24px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 30px rgba(black, 0.1);
|
||||
}
|
||||
|
||||
.tooltip.popover .popover-arrow {
|
||||
border-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.tooltip[aria-hidden="true"] {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s, visibility 0.15s;
|
||||
}
|
||||
|
||||
.tooltip[aria-hidden="false"] {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
</style>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -1 +1,18 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 416 314" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-5895,-5112)">
|
||||
<g id="Artboard1" transform="matrix(1,0,0,1,4187.76,4786.57)">
|
||||
<rect x="0" y="0" width="3840" height="2160" style="fill:none;"/>
|
||||
<g transform="matrix(0.998475,-0.0552112,0,1.00153,504.711,-136.152)">
|
||||
<rect x="1294.87" y="614.156" width="204.453" height="204.719" style="fill:rgb(4,126,251);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.989814,-0.0547323,0.524518,0.471524,802.293,120.929)">
|
||||
<rect x="643.94" y="618.105" width="206.242" height="64.29" style="fill:rgb(123,188,255);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.362077,0.325495,0,1.34467,1135,426.883)">
|
||||
<rect x="1736.88" y="-457.431" width="93.132" height="152.477" style="fill:rgb(49,59,207);"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 539 B After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<span>
|
||||
<v-btn v-tooltip="'Copy to clipboard'" icon small @click="copy">
|
||||
<v-icon small>mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
<input id="text-to-copy" type="hidden" :value="text" />
|
||||
<v-snackbar v-model="snackbar" :timeout="2000" :color="color" text>
|
||||
<div class="text-center">
|
||||
<span class="streamid">{{ text }}</span>
|
||||
{{ message }}
|
||||
</div>
|
||||
</v-snackbar>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["text"],
|
||||
data: () => ({
|
||||
snackbar: false,
|
||||
message: "StreamId copied successfully",
|
||||
color: "success"
|
||||
}),
|
||||
methods: {
|
||||
copy() {
|
||||
this.snackbar = true
|
||||
let textToCopy = document.querySelector("#text-to-copy")
|
||||
textToCopy.setAttribute("type", "text")
|
||||
textToCopy.select()
|
||||
|
||||
try {
|
||||
let result = document.execCommand("copy")
|
||||
this.message = ` copied ${result ? "" : "un"}successfully!`
|
||||
this.color = result ? "success" : "error"
|
||||
} catch (err) {
|
||||
this.message = "Oops, unable to copy!"
|
||||
this.color = "error"
|
||||
}
|
||||
|
||||
/* unselect the range */
|
||||
textToCopy.setAttribute("type", "hidden")
|
||||
window.getSelection().removeAllRanges()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-center" style="position: absolute">
|
||||
<v-avatar class="mt-10" color="background2" size="40">
|
||||
<v-img v-if="user.avatar" :src="user.avatar" />
|
||||
<v-img
|
||||
v-else
|
||||
:src="`https://robohash.org/` + user.id + `.png?size=40x40`"
|
||||
/>
|
||||
</v-avatar>
|
||||
</div>
|
||||
<div class="ml-12">
|
||||
<v-row class="caption">
|
||||
<v-col class="pb-2">
|
||||
<v-icon small>mdi-history</v-icon>
|
||||
|
||||
<strong>You</strong>
|
||||
pushed
|
||||
<span v-if="commit.items">{{ commit.items.length }} commits</span>
|
||||
<span v-else>a commit</span>
|
||||
to
|
||||
<strong>
|
||||
<router-link :to="'streams/' + commit.streamId">
|
||||
{{ commit.streamName }}
|
||||
</router-link>
|
||||
</strong>
|
||||
|
||||
<timeago :datetime="commit.createdAt"></timeago>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card class="mb-3" elevation="0" rounded="lg" color="background2">
|
||||
<v-card-title v-if="!commit.items" class="subtitle-2">
|
||||
{{ commit.message }}
|
||||
</v-card-title>
|
||||
<v-expansion-panels v-else flat color="background2">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header class="pl-4" color="background2">
|
||||
<span class="subtitle-2">
|
||||
{{ commit.message }}
|
||||
</span>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content color="background2">
|
||||
<v-list dense color="background2">
|
||||
<v-list-item v-for="(item, i) in commit.items" :key="i">
|
||||
<div style="width: 100%">
|
||||
<v-row class="caption">
|
||||
<v-col>
|
||||
<span class="caption">{{ item.message }}</span>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col class="text-right">
|
||||
<timeago :datetime="item.createdAt"></timeago>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider v-if="i < commit.items.length - 1"></v-divider>
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
commit: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-center" style="position: absolute">
|
||||
<v-avatar class="mt-10" color="background2" size="40">
|
||||
<v-img v-if="user.avatar" :src="user.avatar" />
|
||||
<v-img
|
||||
v-else
|
||||
:src="`https://robohash.org/` + user.id + `.png?size=40x40`"
|
||||
/>
|
||||
</v-avatar>
|
||||
</div>
|
||||
<div class="ml-12">
|
||||
<v-row class="caption">
|
||||
<v-col cols="12" class="pb-2">
|
||||
<v-icon small>mdi-compare-vertical</v-icon>
|
||||
|
||||
<strong>You</strong>
|
||||
created a new stream
|
||||
<timeago :datetime="stream.createdAt"></timeago>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card class="mb-3" elevation="0" rounded="lg" color="background2">
|
||||
<v-row>
|
||||
<v-col cols="7" class="pt-0 pb-0">
|
||||
<v-card-title class="subtitle-2">
|
||||
<router-link :to="'streams/' + stream.id">
|
||||
{{ stream.name }}
|
||||
</router-link>
|
||||
</v-card-title>
|
||||
<v-card-subtitle>
|
||||
{{ stream.description }}
|
||||
</v-card-subtitle>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="5" class="caption text-right">
|
||||
<div class="mt-1 mr-4">
|
||||
<btn-click-copy :text="stream.id"></btn-click-copy>
|
||||
<router-link :to="'streams/' + stream.id" class="streamid">
|
||||
<span>{{ stream.id }}</span>
|
||||
</router-link>
|
||||
|
||||
<!-- <v-icon small>mdi-key-outline</v-icon> -->
|
||||
<span class="ma-2"></span>
|
||||
<v-icon
|
||||
v-tooltip="
|
||||
stream.branches.totalCount +
|
||||
' branch' +
|
||||
(stream.branches.totalCount === 1 ? '' : 'es')
|
||||
"
|
||||
small
|
||||
>
|
||||
mdi-source-branch
|
||||
</v-icon>
|
||||
<span>{{ stream.branches.totalCount }}</span>
|
||||
|
||||
<span class="ma-2"></span>
|
||||
<v-icon
|
||||
v-tooltip="
|
||||
stream.commits.totalCount +
|
||||
' commit' +
|
||||
(stream.commits.totalCount === 1 ? '' : 's')
|
||||
"
|
||||
small
|
||||
>
|
||||
mdi-history
|
||||
</v-icon>
|
||||
<span>{{ stream.commits.totalCount }}</span>
|
||||
|
||||
<span class="ma-2"></span>
|
||||
<v-icon
|
||||
v-tooltip="
|
||||
stream.collaborators.length +
|
||||
' collaborator' +
|
||||
(stream.collaborators.length === 1 ? '' : 's')
|
||||
"
|
||||
small
|
||||
>
|
||||
mdi-account-outline
|
||||
</v-icon>
|
||||
<span>{{ stream.collaborators.length }}</span>
|
||||
|
||||
<span class="ma-2"></span>
|
||||
<v-icon
|
||||
v-if="stream.isPublic"
|
||||
v-tooltip="`Link sharing on`"
|
||||
small
|
||||
>
|
||||
mdi-link
|
||||
</v-icon>
|
||||
<v-icon v-else v-tooltip="`Link sharing off`" small>
|
||||
mdi-link-lock
|
||||
</v-icon>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import BtnClickCopy from "./BtnClickCopy"
|
||||
|
||||
export default {
|
||||
components: { BtnClickCopy },
|
||||
props: {
|
||||
stream: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
isFeed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,151 +0,0 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row class="text-center">
|
||||
<v-col cols="12">
|
||||
<v-img
|
||||
:src="require('../assets/logo.svg')"
|
||||
class="my-3"
|
||||
contain
|
||||
height="200"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col class="mb-4">
|
||||
<h1 class="display-2 font-weight-bold mb-3">
|
||||
Welcome to Vuetify
|
||||
</h1>
|
||||
|
||||
<p class="subheading font-weight-regular">
|
||||
For help and collaboration with other Vuetify developers,
|
||||
<br>please join our online
|
||||
<a
|
||||
href="https://community.vuetifyjs.com"
|
||||
target="_blank"
|
||||
>Discord Community</a>
|
||||
</p>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
class="mb-5"
|
||||
cols="12"
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-3">
|
||||
What's next?
|
||||
</h2>
|
||||
|
||||
<v-row justify="center">
|
||||
<a
|
||||
v-for="(next, i) in whatsNext"
|
||||
:key="i"
|
||||
:href="next.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ next.text }}
|
||||
</a>
|
||||
</v-row>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
class="mb-5"
|
||||
cols="12"
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-3">
|
||||
Important Links
|
||||
</h2>
|
||||
|
||||
<v-row justify="center">
|
||||
<a
|
||||
v-for="(link, i) in importantLinks"
|
||||
:key="i"
|
||||
:href="link.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ link.text }}
|
||||
</a>
|
||||
</v-row>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
class="mb-5"
|
||||
cols="12"
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-3">
|
||||
Ecosystem
|
||||
</h2>
|
||||
|
||||
<v-row justify="center">
|
||||
<a
|
||||
v-for="(eco, i) in ecosystem"
|
||||
:key="i"
|
||||
:href="eco.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ eco.text }}
|
||||
</a>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HelloWorld',
|
||||
|
||||
data: () => ({
|
||||
ecosystem: [
|
||||
{
|
||||
text: 'vuetify-loader',
|
||||
href: 'https://github.com/vuetifyjs/vuetify-loader',
|
||||
},
|
||||
{
|
||||
text: 'github',
|
||||
href: 'https://github.com/vuetifyjs/vuetify',
|
||||
},
|
||||
{
|
||||
text: 'awesome-vuetify',
|
||||
href: 'https://github.com/vuetifyjs/awesome-vuetify',
|
||||
},
|
||||
],
|
||||
importantLinks: [
|
||||
{
|
||||
text: 'Documentation',
|
||||
href: 'https://vuetifyjs.com',
|
||||
},
|
||||
{
|
||||
text: 'Chat',
|
||||
href: 'https://community.vuetifyjs.com',
|
||||
},
|
||||
{
|
||||
text: 'Made with Vuetify',
|
||||
href: 'https://madewithvuejs.com/vuetify',
|
||||
},
|
||||
{
|
||||
text: 'Twitter',
|
||||
href: 'https://twitter.com/vuetifyjs',
|
||||
},
|
||||
{
|
||||
text: 'Articles',
|
||||
href: 'https://medium.com/vuetify',
|
||||
},
|
||||
],
|
||||
whatsNext: [
|
||||
{
|
||||
text: 'Explore components',
|
||||
href: 'https://vuetifyjs.com/components/api-explorer',
|
||||
},
|
||||
{
|
||||
text: 'Select a layout',
|
||||
href: 'https://vuetifyjs.com/layout/pre-defined',
|
||||
},
|
||||
{
|
||||
text: 'Frequently Asked Questions',
|
||||
href: 'https://vuetifyjs.com/getting-started/frequently-asked-questions',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="7">
|
||||
<div class="subtitle-2">
|
||||
<router-link :to="'/streams/' + streamId + '/commits/' + commit.id">
|
||||
{{ commit.message }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="caption">
|
||||
{{ commit.authorName }} committed
|
||||
<timeago :datetime="commit.createdAt"></timeago>
|
||||
</div>
|
||||
</v-col>
|
||||
<!-- <v-spacer></v-spacer> -->
|
||||
<v-col cols="5" class="caption text-right">
|
||||
<div>
|
||||
<span class="streamid">
|
||||
<router-link :to="'/streams/' + streamId + '/commits/' + commit.id">
|
||||
{{ commit.id }}
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["commit", "streamId"]
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="7">
|
||||
<div class="subtitle-2">
|
||||
<router-link :to="'streams/' + stream.id">
|
||||
{{ stream.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="caption">
|
||||
{{ stream.description }}
|
||||
</div>
|
||||
</v-col>
|
||||
<!-- <v-spacer></v-spacer> -->
|
||||
<v-col cols="5" class="caption text-right">
|
||||
<div>
|
||||
<btn-click-copy :text="stream.id"></btn-click-copy>
|
||||
|
||||
<span class="streamid">
|
||||
<router-link :to="'streams/' + stream.id">
|
||||
<span>{{ stream.id }}</span>
|
||||
</router-link>
|
||||
</span>
|
||||
|
||||
<span class="ma-2"></span>
|
||||
<v-icon
|
||||
v-tooltip="
|
||||
stream.branches.totalCount +
|
||||
' branch' +
|
||||
(stream.branches.totalCount === 1 ? '' : 'es')
|
||||
"
|
||||
small
|
||||
>
|
||||
mdi-source-branch
|
||||
</v-icon>
|
||||
|
||||
<span>{{ stream.branches.totalCount }}</span>
|
||||
|
||||
<span class="ma-2"></span>
|
||||
<v-icon
|
||||
v-tooltip="
|
||||
stream.commits.totalCount +
|
||||
' commit' +
|
||||
(stream.commits.totalCount === 1 ? '' : 's')
|
||||
"
|
||||
small
|
||||
>
|
||||
mdi-history
|
||||
</v-icon>
|
||||
|
||||
<span>{{ stream.commits.totalCount }}</span>
|
||||
|
||||
<span class="ma-2"></span>
|
||||
<v-icon
|
||||
v-tooltip="
|
||||
stream.collaborators.length +
|
||||
' collaborator' +
|
||||
(stream.collaborators.length === 1 ? '' : 's')
|
||||
"
|
||||
small
|
||||
>
|
||||
mdi-account-outline
|
||||
</v-icon>
|
||||
|
||||
<span>{{ stream.collaborators.length }}</span>
|
||||
|
||||
<span class="ma-2"></span>
|
||||
<v-icon v-if="stream.isPublic" v-tooltip="`Link sharing on`" small>
|
||||
mdi-link
|
||||
</v-icon>
|
||||
<v-icon v-else v-tooltip="`Link sharing off`" small>
|
||||
mdi-link-lock
|
||||
</v-icon>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 grey--text text--lighten-1">
|
||||
Created
|
||||
<timeago :datetime="stream.createdAt"></timeago>
|
||||
, updated
|
||||
<timeago :datetime="stream.updatedAt"></timeago>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
import BtnClickCopy from "./BtnClickCopy"
|
||||
|
||||
export default {
|
||||
components: { BtnClickCopy },
|
||||
props: {
|
||||
stream: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<v-row align="center">
|
||||
<v-col cols="1">
|
||||
<v-avatar color="background" size="40">
|
||||
<v-img v-if="user.avatar" :src="user.avatar" />
|
||||
<v-img
|
||||
v-else
|
||||
:src="`https://robohash.org/` + user.id + `.png?size=40x40`"
|
||||
/>
|
||||
</v-avatar>
|
||||
</v-col>
|
||||
<v-col cols="7">
|
||||
<div class="subtitle-2">
|
||||
<!-- <router-link :to="'streams/' + stream.id"> -->
|
||||
{{ user.name }}
|
||||
<!-- </router-link> -->
|
||||
</div>
|
||||
<div class="caption">
|
||||
{{ user.company }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="2" justify="center" class="caption">
|
||||
<i>
|
||||
{{ user.role.replace("stream:", "") }}
|
||||
</i>
|
||||
</v-col>
|
||||
<v-col cols="2" justify="center">
|
||||
<v-btn
|
||||
v-if="!isUniqueStreamOwner"
|
||||
icon
|
||||
small
|
||||
@click="userRemoveClick(user.id)"
|
||||
>
|
||||
<v-icon small color="error">mdi-account-remove-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["user", "userRemoveClick", "isUniqueStreamOwner"]
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
class="pa-4 text-center"
|
||||
style="position: relative"
|
||||
color="background2"
|
||||
>
|
||||
<v-card-title class="justify-center pb-0">
|
||||
<v-avatar color="background" size="64">
|
||||
<v-img v-if="user.avatar" :src="user.avatar" />
|
||||
<v-img
|
||||
v-else
|
||||
:src="`https://robohash.org/` + user.id + `.png?size=64x64`"
|
||||
/>
|
||||
</v-avatar>
|
||||
</v-card-title>
|
||||
<v-card-title class="justify-center">
|
||||
{{ user.name }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="subtitle-1">{{ user.company }}</p>
|
||||
<p>
|
||||
{{ user.bio }}
|
||||
</p>
|
||||
<span class="streamid">{{ user.id }}</span>
|
||||
</v-card-text>
|
||||
<v-btn
|
||||
v-tooltip="'Edit profile'"
|
||||
small
|
||||
icon
|
||||
style="position: absolute; right: 15px; top: 15px"
|
||||
@click="editUser"
|
||||
>
|
||||
<v-icon small>mdi-pencil-outline</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<user-dialog ref="editUserDialog"></user-dialog>
|
||||
</v-card>
|
||||
|
||||
<v-card
|
||||
rounded="lg"
|
||||
class="mt-5 pa-4 text-center"
|
||||
style="position: relative"
|
||||
elevation="0"
|
||||
color="background2"
|
||||
>
|
||||
<v-card-title class="justify-center text-wrap">
|
||||
{{ serverInfo.name }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="subtitle-1">{{ serverInfo.company }}</p>
|
||||
<p>{{ serverInfo.description }}</p>
|
||||
<p v-if="serverInfo.adminContact">
|
||||
Contact: {{ serverInfo.adminContact }}
|
||||
</p>
|
||||
<code v-if="serverInfo.canonicalUrl">
|
||||
{{ serverInfo.canonicalUrl }}
|
||||
</code>
|
||||
</v-card-text>
|
||||
<v-btn
|
||||
v-if="user.role === `server:admin`"
|
||||
v-tooltip="'Edit server information'"
|
||||
small
|
||||
icon
|
||||
style="position: absolute; right: 15px; top: 15px"
|
||||
@click="editServer"
|
||||
>
|
||||
<v-icon small>mdi-pencil-outline</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<server-dialog ref="editServerDialog"></server-dialog>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import userQuery from "../graphql/user.gql"
|
||||
import serverQuery from "../graphql/server.gql"
|
||||
import gql from "graphql-tag"
|
||||
import UserDialog from "../components/dialogs/UserDialog"
|
||||
import ServerDialog from "../components/dialogs/ServerDialog"
|
||||
|
||||
export default {
|
||||
components: { UserDialog, ServerDialog },
|
||||
data: () => ({ user: {}, serverInfo: {} }),
|
||||
apollo: {
|
||||
user: {
|
||||
prefetch: true,
|
||||
query: userQuery
|
||||
},
|
||||
serverInfo: {
|
||||
prefetch: true,
|
||||
query: serverQuery
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editUser() {
|
||||
this.$refs.editUserDialog.open(this.user).then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
console.log(dialog)
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation userUpdate($myUser: UserUpdateInput!) {
|
||||
userUpdate(user: $myUser)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
myUser: { ...dialog.user }
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
this.$apollo.queries.user.refetch()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
editServer() {
|
||||
this.$refs.editServerDialog.open(this.serverInfo).then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
console.log(dialog)
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation serverInfoUpdate($myServerInfo: ServerInfoUpdateInput!) {
|
||||
serverInfoUpdate(info: $myServerInfo)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
myServerInfo: { ...dialog.server }
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
this.$apollo.queries.serverInfo.refetch()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div v-if="stream">
|
||||
<v-card rounded="lg" class="pa-4" elevation="0" color="background2">
|
||||
<v-card-title class="mr-8">
|
||||
{{ stream.name }}
|
||||
</v-card-title>
|
||||
<v-btn
|
||||
v-tooltip="'Edit stream details'"
|
||||
small
|
||||
icon
|
||||
style="position: absolute; right: 15px; top: 15px"
|
||||
@click="editStream"
|
||||
>
|
||||
<v-icon small>mdi-pencil-outline</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<stream-dialog ref="streamDialog"></stream-dialog>
|
||||
|
||||
<v-card-text>
|
||||
<p class="subtitle-1 font-weight-light">{{ stream.description }}</p>
|
||||
|
||||
<p>
|
||||
<btn-click-copy :text="stream.id"></btn-click-copy>
|
||||
|
||||
<span class="streamid">
|
||||
<router-link :to="'/streams/' + stream.id">
|
||||
{{ stream.id }}
|
||||
</router-link>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<v-icon small>mdi-source-branch</v-icon>
|
||||
|
||||
<span>
|
||||
{{ stream.branches.totalCount }}
|
||||
branch{{ stream.branches.totalCount === 1 ? "" : "es" }}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<v-icon small>mdi-history</v-icon>
|
||||
|
||||
<span>
|
||||
{{ stream.commits.totalCount }}
|
||||
commit{{ stream.commits.totalCount === 1 ? "" : "s" }}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<v-icon small>mdi-account-outline</v-icon>
|
||||
|
||||
<span>{{ stream.collaborators.length }}</span>
|
||||
collaborator{{ stream.collaborators.length === 1 ? "" : "s" }}
|
||||
</p>
|
||||
<p>
|
||||
<span v-if="stream.isPublic">
|
||||
<v-icon small>mdi-link</v-icon>
|
||||
link sharing on
|
||||
</span>
|
||||
<span v-else>
|
||||
<v-icon small>mdi-link-lock</v-icon>
|
||||
link sharing off
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Created
|
||||
<timeago :datetime="stream.createdAt"></timeago>
|
||||
</p>
|
||||
<p>
|
||||
Updated
|
||||
<timeago :datetime="stream.updatedAt"></timeago>
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card rounded="lg" class="mt-5 pa-4" elevation="0" color="background2">
|
||||
<v-card-title class="subtitle-1">Collaborators</v-card-title>
|
||||
<div class="ml-2 mr-2">
|
||||
<v-btn
|
||||
v-if="isStreamOwner"
|
||||
v-tooltip="'Manage collaborators'"
|
||||
small
|
||||
fab
|
||||
color="primary"
|
||||
class="ma-1"
|
||||
elevation="0"
|
||||
@click="shareStream"
|
||||
>
|
||||
<v-icon small>mdi-account-multiple-plus</v-icon>
|
||||
</v-btn>
|
||||
<stream-share-dialog
|
||||
ref="streamShareDialog"
|
||||
:users="stream.collaborators"
|
||||
:stream-id="stream.id"
|
||||
:user-id="user.id"
|
||||
></stream-share-dialog>
|
||||
|
||||
<v-avatar
|
||||
v-for="(collab, i) in stream.collaborators"
|
||||
:key="i"
|
||||
class="ma-1"
|
||||
color="grey lighten-3"
|
||||
size="40"
|
||||
>
|
||||
<v-img v-if="collab.avatar" :src="collab.avatar" />
|
||||
<v-img
|
||||
v-else
|
||||
:src="`https://robohash.org/` + collab.id + `.png?size=40x40`"
|
||||
/>
|
||||
</v-avatar>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import gql from "graphql-tag"
|
||||
import streamQuery from "../graphql/stream.gql"
|
||||
import StreamDialog from "../components/dialogs/StreamDialog"
|
||||
import StreamShareDialog from "../components/dialogs/StreamShareDialog"
|
||||
import BtnClickCopy from "./BtnClickCopy"
|
||||
|
||||
export default {
|
||||
components: { StreamDialog, StreamShareDialog, BtnClickCopy },
|
||||
apollo: {
|
||||
stream: {
|
||||
prefetch: true,
|
||||
query: streamQuery,
|
||||
variables() {
|
||||
// Use vue reactive properties here
|
||||
return {
|
||||
id: this.$route.params.streamId
|
||||
}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
prefetch: true,
|
||||
query: gql`
|
||||
query {
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
data: () => ({ user: {} }),
|
||||
computed: {
|
||||
isStreamOwner() {
|
||||
return (
|
||||
this.stream.collaborators.filter(
|
||||
(x) => x.id === this.user.id && x.role === "stream:owner"
|
||||
).length > 0
|
||||
)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
user(val) {
|
||||
//console.log(val)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
shareStream() {
|
||||
this.$refs.streamShareDialog.open()
|
||||
},
|
||||
editStream() {
|
||||
this.$refs.streamDialog.open(this.stream).then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
|
||||
//DELETE STREAM
|
||||
if (dialog.delete) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation streamDelete($id: String!) {
|
||||
streamDelete(id: $id)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: this.stream.id
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
this.$router.push({ name: "streams" })
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//EDIT STREAM
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation streamUpdate($myStream: StreamUpdateInput!) {
|
||||
streamUpdate(stream: $myStream)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
myStream: { ...dialog.stream }
|
||||
//isPublic: dialog.stream.isPublic //TODO: this is not working https://github.com/specklesystems/Server/issues/30
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<v-dialog v-model="show" width="500" @keydown.esc="cancel">
|
||||
<v-card class="pa-4" color="background2">
|
||||
<v-card-title class="subtitle-1">
|
||||
{{ isEdit ? `Edit` : `New` }} Branch
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pl-2 pr-2 pt-0 pb-0">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="valid"
|
||||
lazy-validation
|
||||
@submit.prevent="agree"
|
||||
>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pb-0">
|
||||
<v-text-field
|
||||
v-model="branch.name"
|
||||
label="Name"
|
||||
:rules="nameRules"
|
||||
required
|
||||
filled
|
||||
:disabled="branch.name == 'main'"
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-0 pb-0">
|
||||
<v-textarea
|
||||
v-model="branch.description"
|
||||
filled
|
||||
rows="2"
|
||||
label="Description"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="isEdit && branch.name != 'main'">
|
||||
<v-col cols="12" class="pt-2 pb-2">
|
||||
<div v-if="!pendingDelete">
|
||||
<v-btn
|
||||
color="error"
|
||||
depressed
|
||||
class="mt-5"
|
||||
@click="pendingDelete = true"
|
||||
>
|
||||
Delete Branch
|
||||
</v-btn>
|
||||
<p
|
||||
class="ml-4 mt-0 pt-0 caption"
|
||||
style="display: inline-flex; width: 250px"
|
||||
>
|
||||
Delete this branch forever, no going back here!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="pendingDelete">
|
||||
<v-btn color="error" depressed @click.native="doDelete">
|
||||
Yes
|
||||
</v-btn>
|
||||
<v-btn class="ml-5" depressed @click="pendingDelete = false">
|
||||
No
|
||||
</v-btn>
|
||||
<p
|
||||
class="ml-4 mt-0 pt-0 caption"
|
||||
style="display: inline-flex; width: 150px"
|
||||
>
|
||||
Are you sure?
|
||||
</p>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click.native="agree">
|
||||
{{ isEdit ? `Save` : `Create` }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["branches"],
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
branch: {},
|
||||
name: "",
|
||||
nameRules: [],
|
||||
description: "",
|
||||
valid: true,
|
||||
isEdit: false,
|
||||
pendingDelete: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.dialog
|
||||
},
|
||||
set(value) {
|
||||
this.dialog = value
|
||||
if (value === false) {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"branch.name"(val) {
|
||||
this.nameRules = []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(branch, streamId) {
|
||||
//set defaults
|
||||
this.dialog = true
|
||||
this.pendingDelete = false
|
||||
this.isEdit = false
|
||||
this.branch = {}
|
||||
|
||||
if (this.$refs.form) this.$refs.form.resetValidation()
|
||||
|
||||
if (branch && streamId) {
|
||||
this.branch = {
|
||||
id: branch.id,
|
||||
streamId: streamId,
|
||||
name: branch.name,
|
||||
description: branch.description
|
||||
}
|
||||
|
||||
this.isEdit = true
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve
|
||||
this.reject = reject
|
||||
})
|
||||
},
|
||||
agree() {
|
||||
//prevents annoying validation message from popping at each keystroke
|
||||
//to be used in conjunction with the watch event above and the timer function
|
||||
//source: https://stackoverflow.com/a/57555332
|
||||
this.nameRules = [
|
||||
(v) => !!v || "Branches need a name too!",
|
||||
(v) =>
|
||||
(v &&
|
||||
this.branches.filter((e) => e.name === v && e.id !== this.branch.id)
|
||||
.length === 0) ||
|
||||
"A branch with this name already exists",
|
||||
(v) => (v && v.length <= 25) || "Name must be less than 25 characters",
|
||||
(v) => (v && v.length >= 3) || "Name must be at least 3 characters"
|
||||
]
|
||||
|
||||
let self = this
|
||||
setTimeout(function () {
|
||||
if (self.$refs.form.validate()) {
|
||||
self.resolve({
|
||||
result: true,
|
||||
branch: self.branch
|
||||
})
|
||||
self.dialog = false
|
||||
}
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
this.resolve({
|
||||
result: false
|
||||
})
|
||||
this.dialog = false
|
||||
},
|
||||
doDelete() {
|
||||
this.resolve({
|
||||
result: true,
|
||||
delete: true
|
||||
})
|
||||
this.dialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<v-dialog v-model="show" width="500" @keydown.esc="cancel">
|
||||
<v-card class="pa-4" color="background2">
|
||||
<v-card-title class="subtitle-1">Edit Commit</v-card-title>
|
||||
|
||||
<v-card-text class="pl-2 pr-2 pt-0 pb-0">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="valid"
|
||||
lazy-validation
|
||||
@submit.prevent="agree"
|
||||
>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pb-0">
|
||||
<v-text-field
|
||||
v-model="commit.message"
|
||||
label="Message"
|
||||
:rules="nameRules"
|
||||
required
|
||||
filled
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn :disabled="!valid" color="primary" text @click.native="agree">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
commit: {},
|
||||
nameRules: [],
|
||||
valid: true
|
||||
}),
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.dialog
|
||||
},
|
||||
set(value) {
|
||||
this.dialog = value
|
||||
if (value === false) {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"commit.name"(val) {
|
||||
this.nameRules = []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(commit, streamId) {
|
||||
this.dialog = true
|
||||
if (this.$refs.form) this.$refs.form.resetValidation()
|
||||
|
||||
this.commit = {
|
||||
message: commit.message,
|
||||
id: commit.id,
|
||||
streamId: streamId
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve
|
||||
this.reject = reject
|
||||
})
|
||||
},
|
||||
agree() {
|
||||
this.nameRules = [
|
||||
(v) => !!v || "Please write a commit message",
|
||||
(v) => (v && v.length >= 3) || "Message must be at least 3 characters"
|
||||
]
|
||||
|
||||
let self = this
|
||||
setTimeout(function () {
|
||||
if (self.$refs.form.validate()) {
|
||||
self.resolve({
|
||||
result: true,
|
||||
commit: self.commit
|
||||
})
|
||||
self.dialog = false
|
||||
}
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
this.resolve({
|
||||
result: false
|
||||
})
|
||||
this.dialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<v-dialog v-model="show" width="500" @keydown.esc="cancel">
|
||||
<v-card class="pa-4" color="background2">
|
||||
<v-card-title class="subtitle-1">Edit Server Info</v-card-title>
|
||||
|
||||
<v-card-text class="pl-2 pr-2 pt-0 pb-0">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="valid"
|
||||
lazy-validation
|
||||
@submit.prevent="agree"
|
||||
>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pb-0">
|
||||
<v-text-field
|
||||
v-model="server.name"
|
||||
label="Name"
|
||||
:rules="nameRules"
|
||||
required
|
||||
filled
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-0 pb-0">
|
||||
<v-text-field
|
||||
v-model="server.company"
|
||||
filled
|
||||
label="Company"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-0 pb-0">
|
||||
<v-textarea
|
||||
v-model="server.description"
|
||||
filled
|
||||
rows="2"
|
||||
label="Description"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-0 pb-0">
|
||||
<v-text-field
|
||||
v-model="server.adminContact"
|
||||
filled
|
||||
label="Admin Contact"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-0 pb-0">
|
||||
<v-text-field
|
||||
v-model="server.termsOfService"
|
||||
filled
|
||||
label="Terms Of Service"
|
||||
placeholder="https://..."
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn :disabled="!valid" color="primary" text @click.native="agree">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
server: {},
|
||||
nameRules: [],
|
||||
valid: true
|
||||
}),
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.dialog
|
||||
},
|
||||
set(value) {
|
||||
this.dialog = value
|
||||
if (value === false) {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"server.name"(val) {
|
||||
this.nameRules = []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(server) {
|
||||
this.dialog = true
|
||||
if (this.$refs.form) this.$refs.form.resetValidation()
|
||||
|
||||
this.server = {
|
||||
name: server.name,
|
||||
company: server.company,
|
||||
description: server.description,
|
||||
termsOfService: server.termsOfService,
|
||||
adminContact: server.adminContact
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve
|
||||
this.reject = reject
|
||||
})
|
||||
},
|
||||
agree() {
|
||||
this.nameRules = [
|
||||
(v) => !!v || "Servers need a name too!",
|
||||
(v) => (v && v.length <= 100) || "Name must be less than 25 characters",
|
||||
(v) => (v && v.length >= 3) || "Name must be at least 3 characters"
|
||||
]
|
||||
|
||||
let self = this
|
||||
setTimeout(function () {
|
||||
if (self.$refs.form.validate()) {
|
||||
self.resolve({
|
||||
result: true,
|
||||
server: self.server
|
||||
})
|
||||
self.dialog = false
|
||||
}
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
this.resolve({
|
||||
result: false
|
||||
})
|
||||
this.dialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<v-dialog v-model="show" width="500" @keydown.esc="cancel">
|
||||
<v-card class="pa-4" color="background2">
|
||||
<v-card-title class="subtitle-1">
|
||||
{{ isEdit ? `Edit` : `New` }} Stream
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pl-2 pr-2 pt-0 pb-0">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="valid"
|
||||
lazy-validation
|
||||
@submit.prevent="agree"
|
||||
>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pb-0">
|
||||
<v-text-field
|
||||
v-model="stream.name"
|
||||
label="Name"
|
||||
:rules="nameRules"
|
||||
required
|
||||
filled
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-0 pb-0">
|
||||
<v-textarea
|
||||
v-model="stream.description"
|
||||
filled
|
||||
rows="2"
|
||||
label="Description"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-0 pb-0">
|
||||
<v-switch
|
||||
v-model="stream.isPublic"
|
||||
:label="
|
||||
`Link sharing ` +
|
||||
(stream.isPublic ? `on` : `off` + ` (not working)`)
|
||||
"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="isEdit" align="center">
|
||||
<v-col cols="12" class="pt-2 pb-2">
|
||||
<div v-if="!pendingDelete">
|
||||
<v-btn
|
||||
color="error"
|
||||
depressed
|
||||
class="mt-5"
|
||||
@click="pendingDelete = true"
|
||||
>
|
||||
Delete Stream
|
||||
</v-btn>
|
||||
<p
|
||||
class="ml-4 mt-0 pt-0 caption"
|
||||
style="display: inline-flex; width: 250px"
|
||||
>
|
||||
Delete this stream forever, no going back here!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="pendingDelete">
|
||||
<v-btn color="error" depressed @click.native="doDelete">
|
||||
Yes
|
||||
</v-btn>
|
||||
<v-btn class="ml-5" depressed @click="pendingDelete = false">
|
||||
No
|
||||
</v-btn>
|
||||
<p
|
||||
class="ml-4 mt-0 pt-0 caption"
|
||||
style="display: inline-flex; width: 150px"
|
||||
>
|
||||
Are you sure?
|
||||
</p>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn :disabled="!valid" color="primary" text @click.native="agree">
|
||||
{{ isEdit ? `Save` : `Create` }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
stream: { isPublic: true },
|
||||
nameRules: [],
|
||||
valid: true,
|
||||
isEdit: false,
|
||||
pendingDelete: false
|
||||
}),
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.dialog
|
||||
},
|
||||
set(value) {
|
||||
this.dialog = value
|
||||
if (value === false) {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"stream.name"(val) {
|
||||
this.nameRules = []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(stream) {
|
||||
//set defaults
|
||||
this.dialog = true
|
||||
this.pendingDelete = false
|
||||
this.isEdit = false
|
||||
this.stream = { isPublic: true }
|
||||
|
||||
if (this.$refs.form) this.$refs.form.resetValidation()
|
||||
|
||||
if (stream) {
|
||||
this.stream = {
|
||||
id: stream.id,
|
||||
name: stream.name,
|
||||
description: stream.description,
|
||||
isPublic: stream.isPublic
|
||||
}
|
||||
|
||||
this.isEdit = true
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve
|
||||
this.reject = reject
|
||||
})
|
||||
},
|
||||
agree() {
|
||||
this.nameRules = [
|
||||
(v) => !!v || "Streams need a name too!",
|
||||
(v) =>
|
||||
(v && v.length <= 100) || "Name must be less than 100 characters",
|
||||
(v) => (v && v.length >= 3) || "Name must be at least 3 characters"
|
||||
]
|
||||
|
||||
let self = this
|
||||
setTimeout(function () {
|
||||
if (self.$refs.form.validate()) {
|
||||
self.resolve({
|
||||
result: true,
|
||||
stream: self.stream
|
||||
})
|
||||
self.dialog = false
|
||||
}
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
this.resolve({
|
||||
result: false
|
||||
})
|
||||
this.dialog = false
|
||||
},
|
||||
|
||||
doDelete() {
|
||||
this.resolve({
|
||||
result: true,
|
||||
delete: true
|
||||
})
|
||||
this.dialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" width="600" @keydown.esc="dialog = false">
|
||||
<v-card class="pa-4" color="background2">
|
||||
<v-card-title class="subtitle-1">Manage collaborators</v-card-title>
|
||||
|
||||
<v-card-text class="pl-2 pr-2 pt-0 pb-0">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pb-0">
|
||||
<v-autocomplete
|
||||
v-model="selectedUsers"
|
||||
:loading="$apollo.loading"
|
||||
:items="items"
|
||||
:search-input.sync="search"
|
||||
:filter="filter"
|
||||
multiple
|
||||
counter="3"
|
||||
chips
|
||||
autofocus
|
||||
hide-no-data
|
||||
hide-details
|
||||
placeholder="Type to search..."
|
||||
item-text="name"
|
||||
return-object
|
||||
clearable
|
||||
cache-items
|
||||
label="Users"
|
||||
item-value="id"
|
||||
>
|
||||
<template #selection="{ attr, on, item, selected }">
|
||||
<v-chip
|
||||
v-bind="attr"
|
||||
:input-value="selected"
|
||||
color="secondary"
|
||||
class="white--text"
|
||||
pill
|
||||
close
|
||||
v-on="on"
|
||||
@click:close="remove(item)"
|
||||
>
|
||||
<v-avatar left color="background">
|
||||
<v-img
|
||||
:src="
|
||||
`https://robohash.org/` + item.id + `.png?size=32x32`
|
||||
"
|
||||
/>
|
||||
</v-avatar>
|
||||
<span v-text="item.name"></span>
|
||||
</v-chip>
|
||||
</template>
|
||||
<template #item="{ item }" color="background">
|
||||
<v-list-item-avatar color="background">
|
||||
<v-img
|
||||
:src="
|
||||
`https://robohash.org/` + item.id + `.png?size=40x40`
|
||||
"
|
||||
/>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
v-text="item.company"
|
||||
></v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mb-5" align="center">
|
||||
<v-col cols="12" class="pt-4 pb-0">
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-select
|
||||
v-model="selectedRole"
|
||||
:items="roles"
|
||||
item-text="name"
|
||||
return-object
|
||||
label="Role"
|
||||
class="mr-5"
|
||||
dense
|
||||
style="width: 40px"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<span class="caption">
|
||||
{{ item.name.replace("stream:", "") }}
|
||||
</span>
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<span class="caption">
|
||||
{{ item.name.replace("stream:", "") }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
<v-btn
|
||||
class="primary mb-3"
|
||||
:disabled="
|
||||
!selectedUsers ||
|
||||
selectedUsers.length === 0 ||
|
||||
!selectedRole
|
||||
"
|
||||
@click="grantStreamPermission"
|
||||
>
|
||||
Add collaborators
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
<div v-if="selectedRole" class="caption text-right">
|
||||
{{ selectedRole.description }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-0 pb-0">
|
||||
<div class="subtitle-1 pb-2">Collaborators</div>
|
||||
<div v-for="(user, i) in stream.collaborators" :key="i">
|
||||
<list-item-user
|
||||
:user="user"
|
||||
:user-remove-click="revokeStreamPermission"
|
||||
:is-unique-stream-owner="isUniqueStreamOwner(user.id)"
|
||||
></list-item-user>
|
||||
<v-divider
|
||||
v-if="i < stream.collaborators.length - 1"
|
||||
></v-divider>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-3 pb-0">
|
||||
<v-banner color="secondary" class="white--text" single-line>
|
||||
<v-avatar slot="icon" color="white" size="32">
|
||||
<v-icon color="secondary">mdi-link</v-icon>
|
||||
</v-avatar>
|
||||
|
||||
Link sharing is
|
||||
<b>ON</b>
|
||||
anyone with a link to this stream is able to view it.
|
||||
</v-banner>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click.native="dialog = false">Close</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
import gql from "graphql-tag"
|
||||
import serverQuery from "../../graphql/server.gql"
|
||||
import streamCollaboratorsQuery from "../../graphql/streamCollaborators.gql"
|
||||
import userSearchQuery from "../../graphql/userSearch.gql"
|
||||
import ListItemUser from "../ListItemUser"
|
||||
|
||||
export default {
|
||||
components: { ListItemUser },
|
||||
props: ["streamId", "userId"],
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
search: "",
|
||||
selectedUsers: null,
|
||||
selectedRole: null,
|
||||
userSearch: { items: [] },
|
||||
serverInfo: { roles: [] },
|
||||
user: {}
|
||||
}),
|
||||
apollo: {
|
||||
stream: {
|
||||
prefetch: true,
|
||||
query: streamCollaboratorsQuery,
|
||||
variables() {
|
||||
// Use vue reactive properties here
|
||||
return {
|
||||
id: this.streamId
|
||||
}
|
||||
}
|
||||
},
|
||||
userSearch: {
|
||||
query: userSearchQuery,
|
||||
variables() {
|
||||
// Use vue reactive properties here
|
||||
return {
|
||||
query: this.search,
|
||||
limit: 25
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return !this.search || this.search.length < 3
|
||||
},
|
||||
debounce: 300
|
||||
},
|
||||
serverInfo: {
|
||||
prefetch: true,
|
||||
query: serverQuery
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
roles() {
|
||||
return this.serverInfo.roles
|
||||
.filter((x) => x.resourceTarget === "streams")
|
||||
.reverse()
|
||||
},
|
||||
items() {
|
||||
let items = []
|
||||
this.userSearch.items.forEach((item) => {
|
||||
if (this.stream.collaborators.map((x) => x.id).indexOf(item.id) === -1)
|
||||
items.push(item)
|
||||
})
|
||||
return items
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedUsers(val) {
|
||||
//console.log(val)
|
||||
this.search = ""
|
||||
},
|
||||
roles(val) {
|
||||
this.selectedRole = this.roles[0]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.dialog = true
|
||||
},
|
||||
//filters out cached items that have been added already
|
||||
//the cache-items prop is REQUIRED when using async items and a multiple prom
|
||||
filter(item) {
|
||||
return this.stream.collaborators.map((x) => x.id).indexOf(item.id) === -1
|
||||
},
|
||||
remove(item) {
|
||||
console.log(item)
|
||||
const index = this.selectedUsers.map((x) => x.id).indexOf(item.id)
|
||||
if (index >= 0) this.selectedUsers.splice(index, 1)
|
||||
},
|
||||
isUniqueStreamOwner(id) {
|
||||
return (
|
||||
this.userId === id &&
|
||||
this.stream.collaborators.filter((x) => x.role === "stream:owner")
|
||||
.length === 1 &&
|
||||
this.stream.collaborators.filter(
|
||||
(x) => x.id === this.userId && x.role === "stream:owner"
|
||||
).length === 1
|
||||
)
|
||||
},
|
||||
grantStreamPermission() {
|
||||
var promises = []
|
||||
|
||||
this.selectedUsers.forEach((user) => {
|
||||
promises.push(
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation streamGrantPermission(
|
||||
$permissionParams: StreamGrantPermissionInput!
|
||||
) {
|
||||
streamGrantPermission(permissionParams: $permissionParams)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
permissionParams: {
|
||||
streamId: this.streamId,
|
||||
userId: user.id,
|
||||
role: this.selectedRole.name
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
//
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
Promise.all(promises).then(() => {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
this.selectedUsers = []
|
||||
})
|
||||
},
|
||||
revokeStreamPermission(id) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation streamRevokePermission(
|
||||
$permissionParams: StreamRevokePermissionInput!
|
||||
) {
|
||||
streamRevokePermission(permissionParams: $permissionParams)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
permissionParams: {
|
||||
streamId: this.streamId,
|
||||
userId: id
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<v-dialog v-model="show" width="500" @keydown.esc="cancel">
|
||||
<v-card class="pa-4" color="background2">
|
||||
<v-card-title class="subtitle-1">Edit Profile</v-card-title>
|
||||
|
||||
<v-card-text class="pl-2 pr-2 pt-0 pb-0">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="valid"
|
||||
lazy-validation
|
||||
@submit.prevent="agree"
|
||||
>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pb-0">
|
||||
<v-text-field
|
||||
v-model="user.name"
|
||||
label="Name"
|
||||
:rules="nameRules"
|
||||
required
|
||||
filled
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-0 pb-0">
|
||||
<v-text-field
|
||||
v-model="user.company"
|
||||
filled
|
||||
label="Company"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" class="pt-0 pb-0">
|
||||
<v-textarea
|
||||
v-model="user.bio"
|
||||
filled
|
||||
rows="2"
|
||||
label="Bio"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn :disabled="!valid" color="primary" text @click.native="agree">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
user: {},
|
||||
nameRules: [],
|
||||
valid: true
|
||||
}),
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.dialog
|
||||
},
|
||||
set(value) {
|
||||
this.dialog = value
|
||||
if (value === false) {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"user.name"(val) {
|
||||
this.nameRules = []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(user) {
|
||||
this.dialog = true
|
||||
if (this.$refs.form) this.$refs.form.resetValidation()
|
||||
|
||||
this.user = { name: user.name, company: user.company, bio: user.bio }
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve
|
||||
this.reject = reject
|
||||
})
|
||||
},
|
||||
agree() {
|
||||
this.nameRules = [
|
||||
(v) => !!v || "You need a name!",
|
||||
(v) =>
|
||||
(v && v.length <= 100) || "Name must be less than 100 characters",
|
||||
(v) => (v && v.length >= 3) || "Name must be at least 3 characters"
|
||||
]
|
||||
|
||||
let self = this
|
||||
setTimeout(function () {
|
||||
if (self.$refs.form.validate()) {
|
||||
self.resolve({
|
||||
result: true,
|
||||
user: self.user
|
||||
})
|
||||
self.dialog = false
|
||||
}
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
this.resolve({
|
||||
result: false
|
||||
})
|
||||
this.dialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
query Stream($streamid: String!, $id: String!) {
|
||||
stream(id: $streamid) {
|
||||
id
|
||||
name
|
||||
description
|
||||
isPublic
|
||||
createdAt
|
||||
updatedAt
|
||||
collaborators {
|
||||
id
|
||||
name
|
||||
role
|
||||
}
|
||||
branches {
|
||||
totalCount
|
||||
}
|
||||
commits {
|
||||
totalCount
|
||||
}
|
||||
commit(id: $id) {
|
||||
id
|
||||
message
|
||||
authorName
|
||||
authorId
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
description
|
||||
adminContact
|
||||
canonicalUrl
|
||||
roles {
|
||||
name
|
||||
description
|
||||
resourceTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
query Stream($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
isPublic
|
||||
createdAt
|
||||
updatedAt
|
||||
collaborators {
|
||||
id
|
||||
name
|
||||
role
|
||||
company
|
||||
avatar
|
||||
}
|
||||
branches {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
name
|
||||
author {
|
||||
id
|
||||
suuid
|
||||
email
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
}
|
||||
description
|
||||
commits {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
message
|
||||
authorName
|
||||
authorId
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
commits {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
query Stream($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
isPublic
|
||||
collaborators {
|
||||
id
|
||||
name
|
||||
role
|
||||
company
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
query Streams($cursor: String) {
|
||||
streams(cursor: $cursor) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
name
|
||||
description
|
||||
isPublic
|
||||
createdAt
|
||||
updatedAt
|
||||
collaborators {
|
||||
id
|
||||
name
|
||||
company
|
||||
avatar
|
||||
role
|
||||
}
|
||||
commits {
|
||||
totalCount
|
||||
}
|
||||
branches {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
query {
|
||||
user {
|
||||
id
|
||||
email
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
profiles
|
||||
role
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
query {
|
||||
user {
|
||||
id
|
||||
email
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
profiles
|
||||
role
|
||||
streams(limit: 25) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
name
|
||||
description
|
||||
isPublic
|
||||
createdAt
|
||||
updatedAt
|
||||
collaborators {
|
||||
id
|
||||
name
|
||||
company
|
||||
avatar
|
||||
role
|
||||
}
|
||||
commits {
|
||||
totalCount
|
||||
}
|
||||
branches {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
commits(limit: 25) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
message
|
||||
streamId
|
||||
streamName
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
query UserSearch($query: String!, $limit: Int!) {
|
||||
userSearch(query: $query, limit: $limit) {
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,21 @@ import App from './AppFrontend.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import vuetify from './plugins/vuetify';
|
||||
import { createProvider, onLogin } from './vue-apollo'
|
||||
import { createProvider, } from './vue-apollo'
|
||||
import { signIn } from './auth-helpers'
|
||||
import crs from 'crypto-random-string'
|
||||
import VueTimeago from 'vue-timeago'
|
||||
import VTooltip from 'v-tooltip'
|
||||
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
Vue.use( VueTimeago, {
|
||||
locale: 'en' } )
|
||||
|
||||
Vue.use(VTooltip, { defaultDelay: 300})
|
||||
|
||||
|
||||
|
||||
/* Semicolon of Doom */
|
||||
;
|
||||
/* Semicolon of Doom */
|
||||
@@ -16,11 +25,13 @@ Vue.config.productionTip = false
|
||||
( async ( ) => {
|
||||
let result = await signIn( )
|
||||
if ( !result ) return
|
||||
let app = new Vue( {
|
||||
|
||||
new Vue( {
|
||||
router,
|
||||
store,
|
||||
vuetify,
|
||||
apolloProvider: createProvider( ),
|
||||
render: h => h( App )
|
||||
} ).$mount( '#app' )
|
||||
} )( )
|
||||
} )( )
|
||||
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import Vue from 'vue'
|
||||
import Vuetify from 'vuetify/lib'
|
||||
import "@mdi/font/css/materialdesignicons.css"
|
||||
import Vue from "vue"
|
||||
import Vuetify from "vuetify/lib"
|
||||
|
||||
Vue.use( Vuetify )
|
||||
Vue.use(Vuetify)
|
||||
|
||||
export default new Vuetify( {
|
||||
export default new Vuetify({
|
||||
icons: {
|
||||
iconfont: 'mdi'
|
||||
iconfont: "mdi"
|
||||
},
|
||||
theme: {
|
||||
dark: localStorage.getItem("darkModeEnabled") === "dark",
|
||||
themes: {
|
||||
light: {
|
||||
primary: '#262E37',
|
||||
secondary: '#0A66FF',
|
||||
accent: '#0A66FF',
|
||||
error: '#FF5252',
|
||||
info: '#0A66FF',
|
||||
success: '#0A66FF',
|
||||
warning: '#FFC107'
|
||||
primary: "#047EFB", //blue
|
||||
secondary: "#7BBCFF", //light blue
|
||||
accent: "#FCF25E", //yellow
|
||||
error: "#FF5555", //red
|
||||
warning: "#FF9100", //orange
|
||||
info: "#313BCF", //dark blue
|
||||
success: "#4caf50",
|
||||
background: "#eeeeee",
|
||||
background2: "#ffffff",
|
||||
text: "#FFFFFF"
|
||||
},
|
||||
},
|
||||
},
|
||||
} );
|
||||
dark: {
|
||||
primary: "#047EFB", //blue
|
||||
secondary: "#7BBCFF", //light blue
|
||||
accent: "#FCF25E", //yellow
|
||||
error: "#FF5555", //red
|
||||
warning: "#FF9100", //orange
|
||||
info: "#313BCF", //dark blue
|
||||
success: "#4caf50",
|
||||
background: "#3a3b3c",
|
||||
background2: "#303132"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import Vue from "vue"
|
||||
import VueRouter from "vue-router"
|
||||
import Home from "../views/Home.vue"
|
||||
|
||||
Vue.use( VueRouter )
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [ {
|
||||
path: '/auth',
|
||||
name: 'Login',
|
||||
component: ( ) => import( '../views/auth/Login.vue' )
|
||||
},{
|
||||
path: '/auth/login',
|
||||
name: 'Login',
|
||||
component: ( ) => import( '../views/auth/Login.vue' )
|
||||
}, {
|
||||
path: '/auth/register',
|
||||
name: 'Register',
|
||||
component: ( ) => import( '../views/auth/Registration.vue' )
|
||||
}, {
|
||||
path: '/auth/finalize',
|
||||
name: 'AuthorizeApp',
|
||||
component: ( ) => import( '../views/auth/AuthorizeApp.vue' )
|
||||
} ]
|
||||
const routes = [
|
||||
{
|
||||
path: "/auth",
|
||||
name: "Login",
|
||||
component: () => import("../views/auth/Login.vue")
|
||||
},
|
||||
{
|
||||
path: "/auth/login",
|
||||
name: "Login",
|
||||
component: () => import("../views/auth/Login.vue")
|
||||
},
|
||||
{
|
||||
path: "/auth/register",
|
||||
name: "Register",
|
||||
component: () => import("../views/auth/Registration.vue")
|
||||
},
|
||||
{
|
||||
path: "/auth/finalize",
|
||||
name: "AuthorizeApp",
|
||||
component: () => import("../views/auth/AuthorizeApp.vue")
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter( {
|
||||
mode: 'history',
|
||||
const router = new VueRouter({
|
||||
mode: "history",
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
} )
|
||||
})
|
||||
|
||||
export default router
|
||||
export default router
|
||||
|
||||
@@ -1,25 +1,78 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import Vue from "vue"
|
||||
import VueRouter from "vue-router"
|
||||
|
||||
Vue.use( VueRouter )
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [ {
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: ( ) => import( '../views/Home.vue' )
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
meta: {
|
||||
title: "Home | Speckle"
|
||||
},
|
||||
component: () => import("../views/Home.vue")
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: ( ) => import( '../views/About.vue' )
|
||||
path: "/streams",
|
||||
name: "streams",
|
||||
meta: {
|
||||
title: "Streams | Speckle"
|
||||
},
|
||||
component: () => import("../views/Streams.vue")
|
||||
},
|
||||
{
|
||||
path: "/streams/:streamId",
|
||||
name: "stream",
|
||||
meta: {
|
||||
title: "Stream | Speckle"
|
||||
},
|
||||
component: () => import("../views/Stream.vue")
|
||||
},
|
||||
{
|
||||
path: "/streams/:streamId/commits/:commitId",
|
||||
name: "commit",
|
||||
meta: {
|
||||
title: "Commit | Speckle"
|
||||
},
|
||||
component: () => import("../views/Commit.vue")
|
||||
},
|
||||
{
|
||||
path: "/help",
|
||||
name: "help",
|
||||
meta: {
|
||||
title: "Help | Speckle"
|
||||
},
|
||||
component: () => import("../views/Help.vue")
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
name: "about",
|
||||
meta: {
|
||||
title: "About | Speckle"
|
||||
},
|
||||
component: () => import("../views/About.vue")
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
name: "notfound",
|
||||
meta: {
|
||||
title: "Not Found | Speckle"
|
||||
},
|
||||
component: () => import("../views/NotFound.vue")
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter( {
|
||||
mode: 'history',
|
||||
const router = new VueRouter({
|
||||
mode: "history",
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
} )
|
||||
})
|
||||
|
||||
export default router
|
||||
//TODO: include stream name in page title eg `My Cool Stream | Speckle`
|
||||
router.afterEach((to, from) => {
|
||||
Vue.nextTick(() => {
|
||||
document.title = (to.meta && to.meta.title) || "Speckle"
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import Vue from "vue"
|
||||
import Vuex from "vuex"
|
||||
import gql from "graphql-tag"
|
||||
|
||||
Vue.use(Vuex)
|
||||
Vue.use( Vuex )
|
||||
|
||||
export default new Vuex.Store({
|
||||
export default new Vuex.Store( {
|
||||
state: {
|
||||
user: {}
|
||||
},
|
||||
mutations: {
|
||||
SET_USER( state, value ) {
|
||||
state.user = value
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
// async getUser( { commit } ) {
|
||||
// let user = gql `{ userQuery }`
|
||||
// console.log( user )
|
||||
// commit( 'SET_USER', user )
|
||||
// }
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
})
|
||||
modules: {}
|
||||
} )
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row v-if="stream">
|
||||
<v-col cols="3">
|
||||
<sidebar-stream :stream="stream"></sidebar-stream>
|
||||
</v-col>
|
||||
<v-col cols="9">
|
||||
<v-row>
|
||||
<v-col class="pt-0">
|
||||
<v-card class="pa-5" elevation="0" rounded="lg">
|
||||
<v-card-title class="mr-8">
|
||||
{{ stream.commit.message }}
|
||||
</v-card-title>
|
||||
<!-- TODO need an endpoint to get a commit by ID
|
||||
-->
|
||||
<v-subheader class="text-uppercase">WORK IN PROGRESS</v-subheader>
|
||||
|
||||
<commit-dialog ref="commitDialog"></commit-dialog>
|
||||
<v-btn
|
||||
v-tooltip="'Edit commit details'"
|
||||
small
|
||||
icon
|
||||
style="position: absolute; right: 15px; top: 15px"
|
||||
@click="editBranch"
|
||||
>
|
||||
<v-icon small>mdi-pencil-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import gql from "graphql-tag"
|
||||
import SidebarStream from "../components/SidebarStream"
|
||||
import streamCommitQuery from "../graphql/commit.gql"
|
||||
import CommitDialog from "../components/dialogs/CommitDialog"
|
||||
|
||||
export default {
|
||||
name: "Commit",
|
||||
components: { SidebarStream, CommitDialog },
|
||||
data: () => ({ selectedBranch: 0 }),
|
||||
apollo: {
|
||||
stream: {
|
||||
prefetch: true,
|
||||
query: streamCommitQuery,
|
||||
variables() {
|
||||
// Use vue reactive properties here
|
||||
return {
|
||||
streamid: this.$route.params.streamId,
|
||||
id: this.$route.params.commitId
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
watch: {
|
||||
stream(val) {
|
||||
console.log(val)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editBranch() {
|
||||
this.$refs.commitDialog
|
||||
.open(this.stream.commit, this.stream.id)
|
||||
.then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation commitUpdate($myCommit: CommitUpdateInput!) {
|
||||
commitUpdate(commit: $myCommit)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
myCommit: { ...dialog.commit }
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.v-item-group {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="3">
|
||||
<sidebar-home></sidebar-home>
|
||||
</v-col>
|
||||
<v-col cols="9">
|
||||
<v-sheet rounded="lg" class="pa-15 text-center" color="background2">
|
||||
<h1>Need Help?</h1>
|
||||
<p class="ma-10 subtitle-1 font-weight-light">
|
||||
Get free help from the
|
||||
<a href="https://discourse.speckle.works/">
|
||||
Speckle Community forum
|
||||
</a>
|
||||
or
|
||||
<a href="mailto:hello@speckle.systems">contact us</a>
|
||||
to set up a support agreement and one of our engineers will help
|
||||
right away 🚀!
|
||||
</p>
|
||||
</v-sheet>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import SidebarHome from "../components/SidebarHome"
|
||||
|
||||
export default {
|
||||
components: { SidebarHome }
|
||||
}
|
||||
</script>
|
||||
+141
-12
@@ -1,22 +1,151 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Hello, {{user.name}}! <code>{{user.id}}</code></h1>
|
||||
</div>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="3">
|
||||
<sidebar-home></sidebar-home>
|
||||
</v-col>
|
||||
<v-col v-if="recentActivity" cols="9">
|
||||
<v-row>
|
||||
<v-col class="pt-0">
|
||||
<v-card class="pa-5" elevation="0" rounded="lg" color="background2">
|
||||
<v-subheader class="text-uppercase">Recent activity:</v-subheader>
|
||||
<v-chip-group
|
||||
v-model="selectedActivity"
|
||||
mandatory
|
||||
active-class="primary--text text--accent-1"
|
||||
>
|
||||
<v-chip class="ml-3 mb-3" small>all activity</v-chip>
|
||||
<v-chip class="mb-3" small>streams</v-chip>
|
||||
<v-chip class="mb-3" small>commits</v-chip>
|
||||
</v-chip-group>
|
||||
|
||||
<div class="clear"></div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col class="ml-0 pt-0">
|
||||
<div v-for="(activity, i) in recentActivity" :key="i">
|
||||
<feed-stream
|
||||
v-if="activity.__typename === 'Stream'"
|
||||
:stream="activity"
|
||||
:user="user"
|
||||
></feed-stream>
|
||||
<feed-commit
|
||||
v-else-if="activity.__typename === 'CommitCollectionUserNode'"
|
||||
:commit="activity"
|
||||
:user="user"
|
||||
></feed-commit>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
import HelloWorld from '@/components/HelloWorld.vue'
|
||||
import gql from 'graphql-tag'
|
||||
import SidebarHome from "../components/SidebarHome"
|
||||
import FeedStream from "../components/FeedStream"
|
||||
import FeedCommit from "../components/FeedCommit"
|
||||
import userFeedQuery from "../graphql/userFeed.gql"
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
name: "Home",
|
||||
components: { SidebarHome, FeedStream, FeedCommit },
|
||||
apollo: {
|
||||
user: {
|
||||
query: gql ` query { user { name id } } `
|
||||
prefetch: true,
|
||||
query: userFeedQuery
|
||||
}
|
||||
},
|
||||
data: ( ) => ( {
|
||||
user: { name: null, id: null }
|
||||
} )
|
||||
data: () => ({ selectedActivity: 0, user: {} }),
|
||||
computed: {
|
||||
recentActivity() {
|
||||
let activity = []
|
||||
let activityGrouped = []
|
||||
|
||||
if (
|
||||
this.user.streams &&
|
||||
this.user.streams.items &&
|
||||
this.selectedActivity != 2
|
||||
) {
|
||||
activity.push(...this.user.streams.items)
|
||||
}
|
||||
|
||||
if (
|
||||
this.user.commits &&
|
||||
this.user.commits.items &&
|
||||
this.selectedActivity != 1
|
||||
) {
|
||||
activity.push(...this.user.commits.items)
|
||||
}
|
||||
|
||||
if (activity.length === 1) return activity
|
||||
|
||||
activity.sort(this.compareUpdates)
|
||||
|
||||
let group = []
|
||||
for (let i = 0; i < activity.length; i++) {
|
||||
//first item
|
||||
if (i === 0) {
|
||||
group.push(activity[i])
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
group[0].__typename === "CommitCollectionUserNode" &&
|
||||
group[0].streamId === activity[i].streamId
|
||||
) {
|
||||
group.push(activity[i])
|
||||
} else {
|
||||
if (group.length > 1) {
|
||||
activityGrouped.push({
|
||||
streamName: group[0].streamName,
|
||||
streamId: group[0].streamId,
|
||||
createdAt: group[0].createdAt,
|
||||
message: group[0].message,
|
||||
__typename: "CommitCollectionUserNode",
|
||||
items: group
|
||||
})
|
||||
} else activityGrouped.push(...group)
|
||||
group = []
|
||||
group.push(activity[i])
|
||||
}
|
||||
|
||||
// last item
|
||||
if (i == activity.length - 1) {
|
||||
if (group.length > 1) {
|
||||
activityGrouped.push({
|
||||
streamName: group[0].streamName,
|
||||
streamId: group[0].streamId,
|
||||
createdAt: group[0].createdAt,
|
||||
message: group[0].message,
|
||||
__typename: "CommitCollectionUserNode",
|
||||
items: group
|
||||
})
|
||||
} else activityGrouped.push(...group)
|
||||
}
|
||||
}
|
||||
|
||||
return activityGrouped
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
user(val) {
|
||||
console.log(val)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
compareUpdates(a, b) {
|
||||
if (a.createdAt < b.createdAt) {
|
||||
return 1
|
||||
}
|
||||
if (a.createdAt > b.createdAt) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="3">
|
||||
<sidebar-home></sidebar-home>
|
||||
</v-col>
|
||||
<v-col cols="9">
|
||||
<v-sheet rounded="lg" class="pa-15 text-center">
|
||||
<h1>Page not found ಥ_ಥ</h1>
|
||||
<img :src="'https://robohash.org/' + $route.path + '.png'" />
|
||||
<p class="ma-10 subtitle-1 font-weight-light">
|
||||
We can't find the page you're looking for but hey, here's a cool
|
||||
robot!
|
||||
</p>
|
||||
</v-sheet>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import SidebarHome from "../components/SidebarHome"
|
||||
|
||||
export default {
|
||||
components: { SidebarHome }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row v-if="stream">
|
||||
<v-col cols="3">
|
||||
<sidebar-stream :stream="stream"></sidebar-stream>
|
||||
</v-col>
|
||||
<v-col cols="9">
|
||||
<v-row>
|
||||
<v-col class="pt-0">
|
||||
<v-card class="pa-5" elevation="0" rounded="lg" color="background2">
|
||||
<v-subheader class="text-uppercase">Branches:</v-subheader>
|
||||
|
||||
<v-chip-group
|
||||
v-model="selectedBranch"
|
||||
mandatory
|
||||
class="ml-3"
|
||||
active-class="primary--text text--accent-1"
|
||||
>
|
||||
<v-chip
|
||||
v-for="(branch, i) in branches"
|
||||
:key="i"
|
||||
class="mb-3"
|
||||
small
|
||||
>
|
||||
{{ branch.name }}
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
|
||||
<!-- <v-btn
|
||||
class="mt-1 text-right"
|
||||
color="primary"
|
||||
elevation="0"
|
||||
small
|
||||
@click="newBranch"
|
||||
>
|
||||
<v-icon small class="mr-1">mdi-source-branch-plus</v-icon>
|
||||
new branch
|
||||
</v-btn> -->
|
||||
|
||||
<v-chip-group
|
||||
active-class="primary--text text--accent-1"
|
||||
mandatory
|
||||
>
|
||||
<v-chip small class="mb-3" active @click="newBranch">
|
||||
<v-icon small class="mr-1">mdi-source-branch-plus</v-icon>
|
||||
new branch
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
<branch-dialog
|
||||
ref="branchDialog"
|
||||
:branches="branches"
|
||||
></branch-dialog>
|
||||
|
||||
<div class="clear"></div>
|
||||
|
||||
<p
|
||||
v-if="branches[selectedBranch].description"
|
||||
class="subtitle-1 font-weight-light ml-4 mt-2"
|
||||
>
|
||||
{{ branches[selectedBranch].description }}
|
||||
</p>
|
||||
|
||||
<v-btn
|
||||
v-tooltip="'Edit branch details'"
|
||||
small
|
||||
icon
|
||||
style="position: absolute; right: 15px; top: 15px"
|
||||
@click="editBranch"
|
||||
>
|
||||
<v-icon small>mdi-pencil-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-card rounded="lg" class="pa-5" elevation="0" color="background2">
|
||||
<v-subheader class="text-uppercase">Commits:</v-subheader>
|
||||
|
||||
<v-card-text>
|
||||
<p
|
||||
v-if="branches[selectedBranch].commits.items.length === 0"
|
||||
class="subtitle-1 font-weight-light"
|
||||
>
|
||||
There are no commits in the
|
||||
{{ branches[selectedBranch].name }} branch just yet, try
|
||||
sending something...
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-for="(commit, i) in branches[selectedBranch].commits.items"
|
||||
:key="i"
|
||||
>
|
||||
<list-item-commit
|
||||
:commit="commit"
|
||||
:stream-id="stream.id"
|
||||
></list-item-commit>
|
||||
<v-divider
|
||||
v-if="i < branches[selectedBranch].commits.items.length - 1"
|
||||
></v-divider>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import gql from "graphql-tag"
|
||||
import SidebarStream from "../components/SidebarStream"
|
||||
import BranchDialog from "../components/dialogs/BranchDialog"
|
||||
import ListItemCommit from "../components/ListItemCommit"
|
||||
import streamQuery from "../graphql/stream.gql"
|
||||
|
||||
export default {
|
||||
name: "Stream",
|
||||
components: { SidebarStream, BranchDialog, ListItemCommit },
|
||||
data: () => ({ selectedBranch: 0 }),
|
||||
apollo: {
|
||||
stream: {
|
||||
prefetch: true,
|
||||
query: streamQuery,
|
||||
variables() {
|
||||
// Use vue reactive properties here
|
||||
return {
|
||||
id: this.$route.params.streamId
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
branches() {
|
||||
//reverse without changing original array
|
||||
return this.stream.branches.items.slice().reverse()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
stream(val) {
|
||||
//console.log(val)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
newBranch() {
|
||||
this.$refs.branchDialog.open().then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
console.log(dialog.result)
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation branchCreate($myBranch: BranchCreateInput!) {
|
||||
branchCreate(branch: $myBranch)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
myBranch: {
|
||||
streamId: this.stream.id,
|
||||
...dialog.branch
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
// Result
|
||||
console.log(data)
|
||||
|
||||
this.$apollo.queries.stream.refetch()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
// We restore the initial user input
|
||||
//this.newTag = newTag
|
||||
})
|
||||
})
|
||||
},
|
||||
editBranch() {
|
||||
this.$refs.branchDialog
|
||||
.open(this.branches[this.selectedBranch], this.stream.id)
|
||||
.then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
|
||||
//DELETE BRANCH
|
||||
if (dialog.delete) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation branchDelete($myBranch: BranchDeleteInput!) {
|
||||
branchDelete(branch: $myBranch)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
myBranch: {
|
||||
id: this.branches[this.selectedBranch].id,
|
||||
streamId: this.stream.id
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
this.selectedBranch = 0
|
||||
this.$apollo.queries.stream.refetch()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//EDIT BRANCH
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation branchUpdate($myBranch: BranchUpdateInput!) {
|
||||
branchUpdate(branch: $myBranch)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
myBranch: { ...dialog.branch }
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
this.$apollo.queries.stream.refetch()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.v-item-group {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="3">
|
||||
<sidebar-home></sidebar-home>
|
||||
</v-col>
|
||||
<v-col cols="9">
|
||||
<v-card rounded="lg" class="pa-5" elevation="0" color="background2">
|
||||
<v-card-title>Your Streams</v-card-title>
|
||||
<v-card-actions>
|
||||
<span class="ml-2">
|
||||
You have {{ streams.totalCount }} stream{{
|
||||
streams.totalCount == 1 ? `` : `s`
|
||||
}}
|
||||
in total.
|
||||
</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
class="ml-3 mt-5 text-right"
|
||||
color="primary"
|
||||
elevation="0"
|
||||
small
|
||||
@click="newStream"
|
||||
>
|
||||
<v-icon small class="mr-1">mdi-plus-box-outline</v-icon>
|
||||
new stream
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
<stream-dialog ref="streamDialog"></stream-dialog>
|
||||
|
||||
<v-card-text v-if="streams && streams.items">
|
||||
<div v-for="(stream, i) in streams.items" :key="i">
|
||||
<list-item-stream :stream="stream"></list-item-stream>
|
||||
<v-divider v-if="i < streams.items.length - 1"></v-divider>
|
||||
</div>
|
||||
|
||||
<infinite-loading @infinite="infiniteHandler" v-if="streams.items.length < streams.totalCount">
|
||||
<div slot="no-more">These are all your streams!</div>
|
||||
<div slot="no-results">There are no streams to load</div>
|
||||
</infinite-loading>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import gql from "graphql-tag"
|
||||
import ListItemStream from "../components/ListItemStream"
|
||||
import SidebarHome from "../components/SidebarHome"
|
||||
import StreamDialog from "../components/dialogs/StreamDialog"
|
||||
import streamsQuery from "../graphql/streams.gql"
|
||||
import InfiniteLoading from "vue-infinite-loading"
|
||||
|
||||
export default {
|
||||
name: "Streams",
|
||||
components: { ListItemStream, SidebarHome, StreamDialog, InfiniteLoading },
|
||||
apollo: {
|
||||
streams: {
|
||||
prefetch: true,
|
||||
query: streamsQuery,
|
||||
fetchPolicy: "cache-and-network" //https://www.apollographql.com/docs/react/data/queries/
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
streams: []
|
||||
}),
|
||||
computed: {},
|
||||
watch: {},
|
||||
methods: {
|
||||
infiniteHandler($state) {
|
||||
this.$apollo.queries.streams.fetchMore({
|
||||
variables: {
|
||||
cursor: this.streams.cursor
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
console.log(fetchMoreResult)
|
||||
const newItems = fetchMoreResult.streams.items
|
||||
|
||||
//set vue-infinite state
|
||||
if (newItems.length === 0) $state.complete()
|
||||
else $state.loaded()
|
||||
|
||||
return {
|
||||
streams: {
|
||||
__typename: previousResult.streams.__typename,
|
||||
totalCount: fetchMoreResult.streams.totalCount,
|
||||
cursor: fetchMoreResult.streams.cursor,
|
||||
// Merging the new streams
|
||||
items: [...previousResult.streams.items, ...newItems]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
newStream() {
|
||||
this.$refs.streamDialog.open().then((dialog) => {
|
||||
if (!dialog.result) return
|
||||
console.log(dialog)
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation streamCreate($myStream: StreamCreateInput!) {
|
||||
streamCreate(stream: $myStream)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
myStream: {
|
||||
name: dialog.stream.name,
|
||||
description: dialog.stream.description,
|
||||
isPublic: dialog.stream.isPublic
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
// Result
|
||||
console.log(data)
|
||||
|
||||
this.$apollo.queries.streams.refetch()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Error
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style></style>
|
||||
@@ -1,26 +1,44 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row style='margin-top:-10px;' dense v-if='state===0'>
|
||||
<v-col cols=12>
|
||||
<v-row v-if="state === 0" style="margin-top: -10px" dense>
|
||||
<v-col cols="12">
|
||||
<div>
|
||||
<p class='title font-weight-light text-center'>
|
||||
Authorize <span class='accent--text'><b>{{app.name}}</b></span> by <b>{{app.author}}</b>?
|
||||
<p class="title font-weight-light text-center">
|
||||
Authorize
|
||||
<span class="primary--text">
|
||||
<b>{{ app.name }}</b>
|
||||
</span>
|
||||
by
|
||||
<b>{{ app.author }}</b>
|
||||
?
|
||||
</p>
|
||||
<p class="caption text-center">
|
||||
Clicking allow will redirect you to
|
||||
<i>{{ app.redirectUrl }}</i>
|
||||
</p>
|
||||
<p class='caption text-center'>Clicking allow will redirect you to <i>{{app.redirectUrl }}</i></p>
|
||||
</div>
|
||||
<v-expansion-panels multiple hover tile flat small v-show='!app.firstparty' v-model='panel'>
|
||||
<v-expansion-panels
|
||||
v-show="!app.firstparty"
|
||||
v-model="panel"
|
||||
multiple
|
||||
hover
|
||||
tile
|
||||
flat
|
||||
small
|
||||
>
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header class=' elevation-0'>
|
||||
<v-expansion-panel-header class="elevation-0">
|
||||
<b>Requested permissions:</b>
|
||||
<template v-slot:actions>
|
||||
<v-icon color="accent">mdi-alert-circle</v-icon>
|
||||
<template #actions>
|
||||
<v-icon color="primary">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<ul class='my-3'>
|
||||
<template v-for='scope in app.scopes'>
|
||||
<li :key='scope.name'>
|
||||
<b>{{scope.name}}</b>: {{scope.description}}
|
||||
<ul class="my-3">
|
||||
<template v-for="scope in app.scopes">
|
||||
<li :key="scope.name">
|
||||
<b>{{ scope.name }}</b>
|
||||
: {{ scope.description }}
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
@@ -28,120 +46,120 @@
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-col>
|
||||
<v-col cols='6'>
|
||||
<v-btn block tile color='accent' @click='allow'>Allow</v-btn>
|
||||
<v-col cols="6">
|
||||
<v-btn block tile color="primary" @click="allow">Allow</v-btn>
|
||||
</v-col>
|
||||
<v-col cols='6'>
|
||||
<v-btn block tile color='primary' @click='deny'>Deny</v-btn>
|
||||
<v-col cols="6">
|
||||
<v-btn block tile color="secondary" @click="deny">Deny</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if='state===1'>
|
||||
<v-col cols='12'>
|
||||
<p class='title font-weight-light text-center'>
|
||||
Permissions denied.<br>You can safely close this page.
|
||||
<v-row v-if="state === 1">
|
||||
<v-col cols="12">
|
||||
<p class="title font-weight-light text-center">
|
||||
Permissions denied.
|
||||
<br />
|
||||
You can safely close this page.
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if='state===2'>
|
||||
<v-col cols='12'>
|
||||
<p class='title font-weight-light text-center'>
|
||||
<b>Permissions granted.</b><br>You can now safely close this page.
|
||||
<v-row v-if="state === 2">
|
||||
<v-col cols="12">
|
||||
<p class="title font-weight-light text-center">
|
||||
<b>Permissions granted.</b>
|
||||
<br />
|
||||
You can now safely close this page.
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-snackbar v-model="registrationError" multi-line>
|
||||
{{ errorMessage }}
|
||||
<v-btn color="red" text @click="registrationError = false">
|
||||
Close
|
||||
</v-btn>
|
||||
<v-btn color="red" text @click="registrationError = false">Close</v-btn>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import {
|
||||
onLogin
|
||||
} from '../../vue-apollo'
|
||||
import debounce from 'lodash.debounce'
|
||||
export default {
|
||||
name: 'AuthorizeApp',
|
||||
apollo: {
|
||||
app: {
|
||||
query() {
|
||||
return gql ` query { app( id: "${this.appId}") { id name redirectUrl scopes {name description} } } `
|
||||
},
|
||||
skip() {
|
||||
return this.appId === null
|
||||
},
|
||||
result({
|
||||
data,
|
||||
loading,
|
||||
networkStatus
|
||||
}) {
|
||||
if (data.app.firstparty) {
|
||||
let redirectUrl = data.app.redirectUrl === 'self' ? '/' : data.app.redirectUrl
|
||||
try {
|
||||
window.location = `${redirectUrl}?access_code=${this.accessCode}`
|
||||
} catch (err) {
|
||||
// Fetch?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async deny() {
|
||||
this.state = 1
|
||||
window.history.replaceState({}, document.title, '/auth/finalize')
|
||||
fetch(`${this.app.redirectUrl}?success=false`, {
|
||||
method: 'GET'
|
||||
}).then().catch()
|
||||
import gql from "graphql-tag"
|
||||
import { onLogin } from "../../vue-apollo"
|
||||
import debounce from "lodash.debounce"
|
||||
export default {
|
||||
name: "AuthorizeApp",
|
||||
apollo: {
|
||||
app: {
|
||||
query() {
|
||||
return gql` query { app( id: "${this.appId}") { id name redirectUrl scopes {name description} } } `
|
||||
},
|
||||
async allow() {
|
||||
this.state = 2
|
||||
if (this.app.redirectUrl === 'self')
|
||||
window.location = `${location.origin}/?access_code=${this.accessCode}`
|
||||
else {
|
||||
skip() {
|
||||
return this.appId === null
|
||||
},
|
||||
result({ data, loading, networkStatus }) {
|
||||
if (data.app.firstparty) {
|
||||
let redirectUrl =
|
||||
data.app.redirectUrl === "self" ? "/" : data.app.redirectUrl
|
||||
try {
|
||||
window.location = `${this.app.redirectUrl}?access_code=${this.accessCode}`
|
||||
window.location = `${redirectUrl}?access_code=${this.accessCode}`
|
||||
} catch (err) {
|
||||
fetch(`${this.app.redirectUrl}?access_code=${this.accessCode}`, {
|
||||
method: 'GET'
|
||||
}).then().catch()
|
||||
// Fetch?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
state: 0,
|
||||
currentUrl: window.location.origin,
|
||||
panel: [0],
|
||||
registrationError: false,
|
||||
errorMessage: "",
|
||||
appId: null,
|
||||
app: {
|
||||
name: null,
|
||||
author: null,
|
||||
firstparty: null,
|
||||
scopes: []
|
||||
},
|
||||
data: () => ({
|
||||
state: 0,
|
||||
currentUrl: window.location.origin,
|
||||
panel: [0],
|
||||
registrationError: false,
|
||||
errorMessage: '',
|
||||
appId: null,
|
||||
app: {
|
||||
name: null,
|
||||
author: null,
|
||||
firstparty: null,
|
||||
scopes: []
|
||||
},
|
||||
token: null,
|
||||
accessCode: null,
|
||||
}),
|
||||
mounted() {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
this.appId = urlParams.get('appId') || 'spklwebapp'
|
||||
this.accessCode = urlParams.get('access_code')
|
||||
if (!this.accessCode) {
|
||||
this.$router.push({
|
||||
name: "Login",
|
||||
query: {
|
||||
appId: urlParams.get('appId')
|
||||
}
|
||||
})
|
||||
return
|
||||
token: null,
|
||||
accessCode: null
|
||||
}),
|
||||
mounted() {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
this.appId = urlParams.get("appId") || "spklwebapp"
|
||||
this.accessCode = urlParams.get("access_code")
|
||||
if (!this.accessCode) {
|
||||
this.$router.push({
|
||||
name: "Login",
|
||||
query: {
|
||||
appId: urlParams.get("appId")
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async deny() {
|
||||
this.state = 1
|
||||
window.history.replaceState({}, document.title, "/auth/finalize")
|
||||
fetch(`${this.app.redirectUrl}?success=false`, {
|
||||
method: "GET"
|
||||
})
|
||||
.then()
|
||||
.catch()
|
||||
},
|
||||
async allow() {
|
||||
this.state = 2
|
||||
if (this.app.redirectUrl === "self")
|
||||
window.location = `${location.origin}/?access_code=${this.accessCode}`
|
||||
else {
|
||||
try {
|
||||
window.location = `${this.app.redirectUrl}?access_code=${this.accessCode}`
|
||||
} catch (err) {
|
||||
fetch(`${this.app.redirectUrl}?access_code=${this.accessCode}`, {
|
||||
method: "GET"
|
||||
})
|
||||
.then()
|
||||
.catch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,116 +1,170 @@
|
||||
<template>
|
||||
<v-container fluid v-if='hasLocalStrategy'>
|
||||
<v-form ref='form'>
|
||||
<v-row style='margin-top:-10px;' dense>
|
||||
<v-col cols=12>
|
||||
<v-text-field label='your email' v-model='form.email' :rules='validation.emailRules' solo></v-text-field>
|
||||
<v-container v-if="hasLocalStrategy" fluid>
|
||||
<v-form ref="form">
|
||||
<v-row style="margin-top: -10px" dense>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="form.email"
|
||||
label="your email"
|
||||
:rules="validation.emailRules"
|
||||
solo
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols=12>
|
||||
<v-text-field label='password' type='password' v-model='form.password' :rules='validation.passwordRules' solo style='margin-top:-12px;'></v-text-field>
|
||||
<v-btn block large color='accent' style='top:-22px;' @click='loginUser'>Log in</v-btn>
|
||||
<p class='text-center'>
|
||||
<v-btn text small block color='accent' :to='{ name: "Register", query: { appId: $route.query.appId, challenge: $route.query.challenge, suuid: $route.query.suuid } }'>Create Account</v-btn>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="form.password"
|
||||
label="password"
|
||||
type="password"
|
||||
:rules="validation.passwordRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
block
|
||||
large
|
||||
color="primary"
|
||||
style="top: -22px"
|
||||
@click="loginUser"
|
||||
>
|
||||
Log in
|
||||
</v-btn>
|
||||
<p class="text-center">
|
||||
<v-btn
|
||||
text
|
||||
small
|
||||
block
|
||||
color="primary"
|
||||
:to="{
|
||||
name: 'Register',
|
||||
query: {
|
||||
appId: $route.query.appId,
|
||||
challenge: $route.query.challenge,
|
||||
suuid: $route.query.suuid
|
||||
}
|
||||
}"
|
||||
>
|
||||
Create Account
|
||||
</v-btn>
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-snackbar v-model="registrationError" multi-line>
|
||||
{{ errorMessage }}
|
||||
<v-btn color="red" text @click="registrationError = false">
|
||||
Close
|
||||
</v-btn>
|
||||
<v-btn color="red" text @click="registrationError = false">Close</v-btn>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import { onLogin } from '../../vue-apollo'
|
||||
import debounce from 'lodash.debounce'
|
||||
import crs from 'crypto-random-string'
|
||||
import gql from "graphql-tag"
|
||||
import { onLogin } from "../../vue-apollo"
|
||||
import debounce from "lodash.debounce"
|
||||
import crs from "crypto-random-string"
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
name: "Login",
|
||||
apollo: {
|
||||
serverInfo: {
|
||||
query: gql ` query { serverInfo { name company adminContact termsOfService scopes { name description } authStrategies { id name color icon url } } } `,
|
||||
},
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
adminContact
|
||||
termsOfService
|
||||
scopes {
|
||||
name
|
||||
description
|
||||
}
|
||||
authStrategies {
|
||||
id
|
||||
name
|
||||
color
|
||||
icon
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
serverInfo: { authStrategies: [] },
|
||||
form: { email: null, password: null },
|
||||
validation: {
|
||||
passwordRules: [(v) => !!v || "Required"],
|
||||
emailRules: [
|
||||
(v) => !!v || "E-mail is required",
|
||||
(v) => /.+@.+\..+/.test(v) || "E-mail must be valid"
|
||||
]
|
||||
},
|
||||
registrationError: false,
|
||||
errorMessage: "",
|
||||
appId: null,
|
||||
serverApp: null,
|
||||
suuid: null
|
||||
}),
|
||||
computed: {
|
||||
hasLocalStrategy( ) {
|
||||
return this.serverInfo.authStrategies.findIndex( s => s.id === 'local' ) !== -1
|
||||
hasLocalStrategy() {
|
||||
return (
|
||||
this.serverInfo.authStrategies.findIndex((s) => s.id === "local") !== -1
|
||||
)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
let appId = urlParams.get("appId")
|
||||
let challenge = urlParams.get("challenge")
|
||||
let suuid = urlParams.get("suuid")
|
||||
this.suuid = suuid
|
||||
|
||||
if (!appId) this.appId = "spklwebapp"
|
||||
else this.appId = appId
|
||||
|
||||
if (!challenge && this.appId === "spklwebapp") {
|
||||
this.challenge = crs({ length: 10 })
|
||||
localStorage.setItem("appChallenge", this.challenge)
|
||||
} else if (challenge) {
|
||||
this.challenge = challenge
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loginUser( ) {
|
||||
async loginUser() {
|
||||
try {
|
||||
let valid = this.$refs.form.validate( )
|
||||
if ( !valid ) throw new Error( 'Form validation failed' )
|
||||
let valid = this.$refs.form.validate()
|
||||
if (!valid) throw new Error("Form validation failed")
|
||||
|
||||
let user = {
|
||||
email: this.form.email,
|
||||
password: this.form.password
|
||||
}
|
||||
|
||||
if ( this.suuid ) user.suuid = this.suuid
|
||||
if (this.suuid) user.suuid = this.suuid
|
||||
|
||||
let res = await fetch( `/auth/local/login?appId=${this.appId}&challenge=${this.challenge}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
redirect: 'follow', // obvs not working
|
||||
body: JSON.stringify( user )
|
||||
} )
|
||||
let res = await fetch(
|
||||
`/auth/local/login?appId=${this.appId}&challenge=${this.challenge}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
redirect: "follow", // obvs not working
|
||||
body: JSON.stringify(user)
|
||||
}
|
||||
)
|
||||
|
||||
if ( res.redirected ) {
|
||||
if (res.redirected) {
|
||||
window.location = res.url
|
||||
}
|
||||
|
||||
if ( !res.ok ) {
|
||||
throw new Error( 'Login failed' )
|
||||
if (!res.ok) {
|
||||
throw new Error("Login failed")
|
||||
}
|
||||
|
||||
} catch ( err ) {
|
||||
} catch (err) {
|
||||
this.errorMessage = err.message
|
||||
this.registrationError = true
|
||||
}
|
||||
}
|
||||
},
|
||||
data: ( ) => ( {
|
||||
serverInfo: { authStrategies: [ ] },
|
||||
form: { email: null, password: null },
|
||||
validation: {
|
||||
passwordRules: [ v => !!v || 'Required' ],
|
||||
emailRules: [
|
||||
v => !!v || 'E-mail is required',
|
||||
v => /.+@.+\..+/.test( v ) || 'E-mail must be valid',
|
||||
],
|
||||
},
|
||||
registrationError: false,
|
||||
errorMessage: '',
|
||||
appId: null,
|
||||
serverApp: null,
|
||||
suuid: null
|
||||
} ),
|
||||
mounted( ) {
|
||||
let urlParams = new URLSearchParams( window.location.search )
|
||||
let appId = urlParams.get( 'appId' )
|
||||
let challenge = urlParams.get( 'challenge' )
|
||||
let suuid = urlParams.get( 'suuid' )
|
||||
this.suuid = suuid
|
||||
|
||||
if ( !appId )
|
||||
this.appId = 'spklwebapp'
|
||||
else
|
||||
this.appId = appId
|
||||
|
||||
if ( !challenge && this.appId === 'spklwebapp' ) {
|
||||
this.challenge = crs( { length: 10 } )
|
||||
localStorage.setItem( 'appChallenge', this.challenge )
|
||||
} else if ( challenge ) {
|
||||
this.challenge = challenge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,114 +1,225 @@
|
||||
<template>
|
||||
<v-container fluid v-if='hasLocalStrategy'>
|
||||
<v-form ref='form'>
|
||||
<v-row style='margin-top:-10px;' dense>
|
||||
<v-col cols=12>
|
||||
<v-text-field label='your email' v-model='form.email' :rules='validation.emailRules' solo></v-text-field>
|
||||
<v-container v-if="hasLocalStrategy" fluid>
|
||||
<v-form ref="form">
|
||||
<v-row style="margin-top: -10px" dense>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="form.email"
|
||||
label="your email"
|
||||
:rules="validation.emailRules"
|
||||
solo
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col xs='6' sm='6'>
|
||||
<v-text-field label='first name' v-model='form.firstName' :rules='validation.nameRules' solo style='margin-top:-12px;'></v-text-field>
|
||||
<v-col xs="6" sm="6">
|
||||
<v-text-field
|
||||
v-model="form.firstName"
|
||||
label="first name"
|
||||
:rules="validation.nameRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col xs='6' sm='6'>
|
||||
<v-text-field label='last name' v-model='form.lastName' :rules='validation.nameRules' solo style='margin-top:-12px;'></v-text-field>
|
||||
<v-col xs="6" sm="6">
|
||||
<v-text-field
|
||||
v-model="form.lastName"
|
||||
label="last name"
|
||||
:rules="validation.nameRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols='12' sm='12'>
|
||||
<v-text-field label='company/team' v-model='form.company' :rules='validation.companyRules' solo style='margin-top:-12px;'></v-text-field>
|
||||
<v-col cols="12" sm="12">
|
||||
<v-text-field
|
||||
v-model="form.company"
|
||||
label="company/team"
|
||||
:rules="validation.companyRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols='12' sm='6'>
|
||||
<v-text-field label='password' type='password' v-model='form.password' :rules='validation.passwordRules' @keydown='debouncedPwdTest' solo style='margin-top:-12px;'></v-text-field>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="form.password"
|
||||
label="password"
|
||||
type="password"
|
||||
:rules="validation.passwordRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
@keydown="debouncedPwdTest"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols='12' sm='6'>
|
||||
<v-text-field label='confirm password' type='password' v-model='form.passwordConf' :rules='validation.passwordRules' solo style='margin-top:-12px;'></v-text-field>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="form.passwordConf"
|
||||
label="confirm password"
|
||||
type="password"
|
||||
:rules="validation.passwordRules"
|
||||
solo
|
||||
style="margin-top: -12px"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols='12' class='py-2 px-2' style='margin-top:-18px;'>
|
||||
<v-row no-gutters align='center'>
|
||||
<v-col cols="12" class="py-2 px-2" style="margin-top: -18px">
|
||||
<v-row no-gutters align="center">
|
||||
<!-- <v-col cols='3' class='caption flex-shrink-1 flex-grow-0'>Strength:</v-col> -->
|
||||
<v-col cols='12' class='flex-grow-1 flex-shrink-0' style="min-width: 100px; max-width: 100%;">
|
||||
<v-progress-linear v-show='true' height=5 class='mt-1 mb-0' v-model="passwordStrength" :color="`${passwordStrength >= 75 ? 'green' : passwordStrength >= 50 ? 'orange' : 'red' }`">
|
||||
</v-progress-linear>
|
||||
<v-col
|
||||
cols="12"
|
||||
class="flex-grow-1 flex-shrink-0"
|
||||
style="min-width: 100px; max-width: 100%"
|
||||
>
|
||||
<v-progress-linear
|
||||
v-show="true"
|
||||
v-model="passwordStrength"
|
||||
height="5"
|
||||
class="mt-1 mb-0"
|
||||
:color="`${
|
||||
passwordStrength >= 75
|
||||
? 'green'
|
||||
: passwordStrength >= 50
|
||||
? 'orange'
|
||||
: 'red'
|
||||
}`"
|
||||
></v-progress-linear>
|
||||
</v-col>
|
||||
<v-col cols='12' class='caption text-center mt-3'>
|
||||
{{this.pwdSuggestions ? this.pwdSuggestions : this.form.password ? 'Looks good.' : 'Password strength' }}
|
||||
<span v-if='this.form.password !== this.form.passwordConf'><b>Passwords do not match.</b></span>
|
||||
<v-col cols="12" class="caption text-center mt-3">
|
||||
{{
|
||||
this.pwdSuggestions
|
||||
? this.pwdSuggestions
|
||||
: this.form.password
|
||||
? "Looks good."
|
||||
: "Password strength"
|
||||
}}
|
||||
<span v-if="this.form.password !== this.form.passwordConf">
|
||||
<b>Passwords do not match.</b>
|
||||
</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col cols=12>
|
||||
<v-btn block large color='accent' style='margin-top:-0px;' @click='registerUser'>Sign Up</v-btn>
|
||||
<p class='text-center'>
|
||||
<v-btn text small block color='accent' :to='{ name: "Login", query: { appId: $route.query.appId, challenge: $route.query.challenge, suuid: $route.query.suuid } }' class='mt-5'>Login</v-btn>
|
||||
<v-col cols="12">
|
||||
<v-btn
|
||||
block
|
||||
large
|
||||
color="primary"
|
||||
style="margin-top: -0px"
|
||||
@click="registerUser"
|
||||
>
|
||||
Sign Up
|
||||
</v-btn>
|
||||
<p class="text-center">
|
||||
<v-btn
|
||||
text
|
||||
small
|
||||
block
|
||||
color="primary"
|
||||
:to="{
|
||||
name: 'Login',
|
||||
query: {
|
||||
appId: $route.query.appId,
|
||||
challenge: $route.query.challenge,
|
||||
suuid: $route.query.suuid
|
||||
}
|
||||
}"
|
||||
class="mt-5"
|
||||
>
|
||||
Login
|
||||
</v-btn>
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-snackbar v-model="registrationError" multi-line>
|
||||
{{ errorMessage }}
|
||||
<v-btn color="red" text @click="registrationError = false">
|
||||
Close
|
||||
</v-btn>
|
||||
<v-btn color="red" text @click="registrationError = false">Close</v-btn>
|
||||
</v-snackbar>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import { onLogin } from '../../vue-apollo'
|
||||
import debounce from 'lodash.debounce'
|
||||
import crs from 'crypto-random-string'
|
||||
import gql from "graphql-tag"
|
||||
import { onLogin } from "../../vue-apollo"
|
||||
import debounce from "lodash.debounce"
|
||||
import crs from "crypto-random-string"
|
||||
|
||||
export default {
|
||||
name: 'Registration',
|
||||
name: "Registration",
|
||||
apollo: {
|
||||
serverInfo: {
|
||||
query: gql ` query { serverInfo { name company adminContact termsOfService scopes { name description } authStrategies { id name color icon url } } } `,
|
||||
},
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
adminContact
|
||||
termsOfService
|
||||
scopes {
|
||||
name
|
||||
description
|
||||
}
|
||||
authStrategies {
|
||||
id
|
||||
name
|
||||
color
|
||||
icon
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasLocalStrategy( ) {
|
||||
return this.serverInfo.authStrategies.findIndex( s => s.id === 'local' ) !== -1
|
||||
hasLocalStrategy() {
|
||||
return (
|
||||
this.serverInfo.authStrategies.findIndex((s) => s.id === "local") !== -1
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
debouncedPwdTest: debounce( async function ( ) {
|
||||
let result = await this.$apollo.query( { query: gql ` query{ userPwdStrength(pwd:"${this.form.password}")}` } )
|
||||
debouncedPwdTest: debounce(async function () {
|
||||
let result = await this.$apollo.query({
|
||||
query: gql` query{ userPwdStrength(pwd:"${this.form.password}")}`
|
||||
})
|
||||
this.passwordStrength = result.data.userPwdStrength.score * 25
|
||||
this.pwdSuggestions = result.data.userPwdStrength.feedback.suggestions[ 0 ]
|
||||
}, 1000 ),
|
||||
async registerUser( ) {
|
||||
this.pwdSuggestions = result.data.userPwdStrength.feedback.suggestions[0]
|
||||
}, 1000),
|
||||
async registerUser() {
|
||||
try {
|
||||
let valid = this.$refs.form.validate( )
|
||||
if ( !valid ) throw new Error( 'Form validation failed' )
|
||||
if ( this.form.password !== this.form.passwordConf ) throw new Error( 'Passwords do not match' )
|
||||
if ( this.passwordStrength < 3 ) throw new Error( 'Password too weak' )
|
||||
let valid = this.$refs.form.validate()
|
||||
if (!valid) throw new Error("Form validation failed")
|
||||
if (this.form.password !== this.form.passwordConf)
|
||||
throw new Error("Passwords do not match")
|
||||
if (this.passwordStrength < 3) throw new Error("Password too weak")
|
||||
|
||||
let user = {
|
||||
email: this.form.email,
|
||||
company: this.form.company,
|
||||
password: this.form.password,
|
||||
name: `${this.form.firstName} ${this.form.lastName}`,
|
||||
name: `${this.form.firstName} ${this.form.lastName}`
|
||||
}
|
||||
|
||||
if( this.suuid ) user.suuid = this.suuid
|
||||
if (this.suuid) user.suuid = this.suuid
|
||||
|
||||
let res = await fetch( `/auth/local/register?appId=${this.appId}&challenge=${this.challenge}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
redirect: 'follow', // obvs not working
|
||||
body: JSON.stringify( user )
|
||||
} )
|
||||
let res = await fetch(
|
||||
`/auth/local/register?appId=${this.appId}&challenge=${this.challenge}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
redirect: "follow", // obvs not working
|
||||
body: JSON.stringify(user)
|
||||
}
|
||||
)
|
||||
|
||||
if ( res.redirected ) {
|
||||
if (res.redirected) {
|
||||
window.location = res.url
|
||||
}
|
||||
} catch ( err ) {
|
||||
} catch (err) {
|
||||
this.errorMessage = err.message
|
||||
this.registrationError = true
|
||||
}
|
||||
}
|
||||
},
|
||||
data: ( ) => ( {
|
||||
serverInfo: { authStrategies: [ ] },
|
||||
data: () => ({
|
||||
serverInfo: { authStrategies: [] },
|
||||
form: {
|
||||
email: null,
|
||||
firstName: null,
|
||||
@@ -118,45 +229,42 @@ export default {
|
||||
passwordConf: null
|
||||
},
|
||||
registrationError: false,
|
||||
errorMessage: '',
|
||||
errorMessage: "",
|
||||
validation: {
|
||||
companyRules: [ v => !!v || 'Required' ],
|
||||
passwordRules: [ v => !!v || 'Required' ],
|
||||
companyRules: [(v) => !!v || "Required"],
|
||||
passwordRules: [(v) => !!v || "Required"],
|
||||
nameRules: [
|
||||
v => !!v || 'Required',
|
||||
v => ( v && v.length <= 10 ) || 'Name must be less than 10 characters',
|
||||
(v) => !!v || "Required",
|
||||
(v) => (v && v.length <= 10) || "Name must be less than 10 characters"
|
||||
],
|
||||
emailRules: [
|
||||
v => !!v || 'E-mail is required',
|
||||
v => /.+@.+\..+/.test( v ) || 'E-mail must be valid',
|
||||
],
|
||||
(v) => !!v || "E-mail is required",
|
||||
(v) => /.+@.+\..+/.test(v) || "E-mail must be valid"
|
||||
]
|
||||
},
|
||||
passwordStrength: 1,
|
||||
pwdSuggestions: null,
|
||||
appId: null,
|
||||
challenge: null,
|
||||
suuid: null
|
||||
} ),
|
||||
mounted( ) {
|
||||
let urlParams = new URLSearchParams( window.location.search )
|
||||
let appId = urlParams.get( 'appId' )
|
||||
let challenge = urlParams.get( 'challenge' )
|
||||
let suuid = urlParams.get( 'suuid' )
|
||||
}),
|
||||
mounted() {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
let appId = urlParams.get("appId")
|
||||
let challenge = urlParams.get("challenge")
|
||||
let suuid = urlParams.get("suuid")
|
||||
|
||||
this.suuid = suuid
|
||||
console.log( this.suuid )
|
||||
if ( !appId )
|
||||
this.appId = 'spklwebapp'
|
||||
else
|
||||
this.appId = appId
|
||||
console.log(this.suuid)
|
||||
if (!appId) this.appId = "spklwebapp"
|
||||
else this.appId = appId
|
||||
|
||||
if ( !challenge && this.appId === 'spklwebapp' ) {
|
||||
this.challenge = crs( { length: 10 } )
|
||||
localStorage.setItem( 'appChallenge', this.challenge )
|
||||
} else if ( challenge ) {
|
||||
if (!challenge && this.appId === "spklwebapp") {
|
||||
this.challenge = crs({ length: 10 })
|
||||
localStorage.setItem("appChallenge", this.challenge)
|
||||
} else if (challenge) {
|
||||
this.challenge = challenge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
+12
-13
@@ -1,27 +1,26 @@
|
||||
module.exports = {
|
||||
pages: {
|
||||
app: {
|
||||
entry: 'src/main-frontend.js',
|
||||
title: 'Speckle!',
|
||||
template: 'public/app.html',
|
||||
filename: 'app.html'
|
||||
entry: "src/main-frontend.js",
|
||||
title: "Speckle",
|
||||
template: "public/app.html",
|
||||
filename: "app.html"
|
||||
},
|
||||
auth: {
|
||||
entry: 'src/main-auth.js',
|
||||
title: 'Speckle Authentication',
|
||||
template: 'public/auth.html',
|
||||
filename: 'auth.html'
|
||||
entry: "src/main-auth.js",
|
||||
title: "Speckle Authentication",
|
||||
template: "public/auth.html",
|
||||
filename: "auth.html"
|
||||
}
|
||||
},
|
||||
devServer: {
|
||||
historyApiFallback: {
|
||||
rewrites: [
|
||||
{ from: /^\/$/, to: '/app.html' },
|
||||
{ from: /\/auth/, to: '/auth.html' }
|
||||
{ from: /^\/$/, to: "/app.html" },
|
||||
{ from: /\/auth/, to: "/auth.html" },
|
||||
{ from: /./, to: "/app.html" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"transpileDependencies": [
|
||||
"vuetify"
|
||||
]
|
||||
transpileDependencies: ["vuetify"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ let migrationDirs = walk( './modules' )
|
||||
module.exports = {
|
||||
test: {
|
||||
client: 'pg',
|
||||
connection: process.env.POSTGRES_URL || 'postgres://localhost/speckle2_test',
|
||||
connection: 'postgres://localhost/speckle2_test',
|
||||
migrations: {
|
||||
directory: migrationDirs
|
||||
},
|
||||
|
||||
@@ -43,7 +43,7 @@ exports.up = async knex => {
|
||||
} )
|
||||
|
||||
const scopes = await knex( 'scopes' ).select( '*' )
|
||||
const webAppScopes = scopes.filter( s => s.name !== 'server:setup' ).map( s => ( { appId: 'spklwebapp', scopeName: s.name } ) )
|
||||
const webAppScopes = scopes.map( s => ( { appId: 'spklwebapp', scopeName: s.name } ) )
|
||||
await knex( 'server_apps_scopes' ).insert( webAppScopes )
|
||||
|
||||
//
|
||||
|
||||
@@ -79,7 +79,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
async userEdit( parent, args, context, info ) {
|
||||
async userUpdate( parent, args, context, info ) {
|
||||
await validateServerRole( context, 'server:user' )
|
||||
await updateUser( context.userId, args.user )
|
||||
return true
|
||||
|
||||
@@ -13,7 +13,7 @@ type Branch {
|
||||
id: String!
|
||||
name: String!
|
||||
author: User!
|
||||
description: String!
|
||||
description: String
|
||||
commits(limit: Int! = 25, cursor: String): CommitCollection
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ type CommitCollectionUserNode {
|
||||
message: String
|
||||
streamId: String
|
||||
streamName: String
|
||||
createdAt: DateTime
|
||||
}
|
||||
|
||||
type BranchCollection {
|
||||
|
||||
@@ -34,7 +34,9 @@ type Scope {
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
serverInfoUpdate( info: ServerInfoUpdateInput! ): Boolean
|
||||
serverInfoUpdate(info: ServerInfoUpdateInput!): Boolean
|
||||
@hasRole(role: "server:admin")
|
||||
@hasScope(scope: "server:setup")
|
||||
}
|
||||
|
||||
input ServerInfoUpdateInput {
|
||||
|
||||
@@ -2,9 +2,13 @@ extend type Query {
|
||||
"""
|
||||
Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
|
||||
"""
|
||||
user( id: String ): User
|
||||
userSearch( query: String!, limit: Int! = 25, cursor: String ): UserSearchResultCollection
|
||||
userPwdStrength( pwd: String! ): JSONObject
|
||||
user(id: String): User
|
||||
userSearch(
|
||||
query: String!
|
||||
limit: Int! = 25
|
||||
cursor: String
|
||||
): UserSearchResultCollection
|
||||
userPwdStrength(pwd: String!): JSONObject
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -25,7 +29,7 @@ type User {
|
||||
|
||||
type UserSearchResultCollection {
|
||||
cursor: String
|
||||
items: [ UserSearchResult ]
|
||||
items: [UserSearchResult]
|
||||
}
|
||||
|
||||
type UserSearchResult {
|
||||
@@ -41,10 +45,10 @@ extend type Mutation {
|
||||
"""
|
||||
Edits a user's profile.
|
||||
"""
|
||||
userEdit(user: UserEditInput!): Boolean!
|
||||
userUpdate(user: UserUpdateInput!): Boolean!
|
||||
}
|
||||
|
||||
input UserEditInput {
|
||||
input UserUpdateInput {
|
||||
name: String
|
||||
company: String
|
||||
bio: String
|
||||
|
||||
@@ -108,10 +108,10 @@ describe( 'GraphQL API Core @core-api', ( ) => {
|
||||
} )
|
||||
|
||||
it( 'Should edit my profile', async ( ) => {
|
||||
const res = await sendRequest( userA.token, { query: `mutation($user:UserEditInput!) { userEdit( user: $user) } `, variables: { user: { name: 'Miticå', bio: 'He never really knows what he is doing.' } } } )
|
||||
const res = await sendRequest( userA.token, { query: `mutation($user:UserUpdateInput!) { userUpdate( user: $user) } `, variables: { user: { name: 'Miticå', bio: 'He never really knows what he is doing.' } } } )
|
||||
expect( res ).to.be.json
|
||||
expect( res.body.errors ).to.not.exist
|
||||
expect( res.body.data.userEdit ).to.equal( true )
|
||||
expect( res.body.data.userUpdate ).to.equal( true )
|
||||
} )
|
||||
} )
|
||||
|
||||
|
||||
Generated
-5
@@ -3624,11 +3624,6 @@
|
||||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
||||
"dev": true
|
||||
},
|
||||
"g": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/g/-/g-2.0.1.tgz",
|
||||
"integrity": "sha1-C1lj69DKcOO8jGdmk0oCGCHIuFc="
|
||||
},
|
||||
"gauge": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
|
||||
@@ -30,7 +30,7 @@ To get started, first clone this repo.
|
||||
To run the **Server** in debug mode:
|
||||
|
||||
- Duplicate and rename `.env-example` to `.env` & fill it in!
|
||||
- Make sure a postgres instance is running locally, with two databases present, named `speckle2_dev` and `speckle2_test`.
|
||||
- Make sure a postgres instance is running locally, with two databases present, named `speckle2_dev` and `speckle2_test` (or whatever needed to match your .env `POSTGRES_URL` variable).
|
||||
- Make sure a redis instance is running locally.
|
||||
- Run `npm install`
|
||||
- Run `npm run dev:server` 🚀
|
||||
|
||||
Reference in New Issue
Block a user