feat(frontend): uniforms stream pages

This commit is contained in:
Matteo Cominetti
2021-07-29 17:00:07 +02:00
parent 19fda04ec3
commit 532ef4e9d4
13 changed files with 521 additions and 177 deletions
@@ -0,0 +1,252 @@
<template>
<v-card v-if="stream" :loading="loading" class="mt-5 pa-4" elevation="0" rounded="lg">
<template slot="progress">
<v-progress-linear indeterminate></v-progress-linear>
</template>
<v-card-title>
<v-icon class="mr-2">mdi-account-plus-outline</v-icon>
<span class="d-inline-block">Add collaborators</span>
</v-card-title>
<v-card-text>
<v-text-field
v-model="search"
autofocus
label="Search for a user"
hint="You will be able to set their roles once they have been added"
persistent-hint
/>
<div v-if="$apollo.loading">Searching.</div>
<v-list
v-if="search && search.length >= 3 && userSearch && userSearch.items"
dense
one-line
class="px-0 mx-0"
>
<v-list-item v-if="filteredSearchResults.length === 0" class="px-0 mx-0">
<v-list-item-content>
<v-list-item-title>
No users found. Note: you can search by name and email.
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item v-if="filteredSearchResults.length === 0" class="px-0 mx-0">
<v-list-item-action>
<v-btn color="primary" @click="showStreamInviteDialog">Invite {{ search }}</v-btn>
</v-list-item-action>
</v-list-item>
<v-list-item
v-for="item in filteredSearchResults"
v-else
:key="item.id"
class="px-0 mx-0"
@click="addCollab(item)"
>
<v-list-item-avatar>
<user-avatar
:id="item.id"
:name="item.name"
:avatar="item.avatar"
:size="25"
class="ml-1"
></user-avatar>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>{{ item.name }}</v-list-item-title>
<v-list-item-subtitle>
{{ item.company ? item.company : 'no company info' }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-icon>mdi-plus</v-icon>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card-text>
<stream-invite-dialog ref="streamInviteDialog" :stream-id="stream.id" :text="search" />
<v-card-title>
<v-icon class="mr-2">mdi-account-group-outline</v-icon>
<span class="d-inline-block">Collaborators ({{ stream.collaborators.length - 1 }})</span>
</v-card-title>
<v-card-text class="px-0">
<p v-if="collaborators.length === 0" class="ml-4">
You don't have collaborators on this stream .
</p>
<v-list v-else>
<v-list-item v-for="user in collaborators" :key="user.id" two-lines>
<v-list-item-icon>
<user-avatar :id="user.id" :avatar="user.avatar" :name="user.name" :size="42" />
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="font-weight-bold">{{ user.name }}</v-list-item-title>
<v-list-item-subtitle>
<!-- Role: {{ user.role.replace('stream:', '') }} -->
<v-select
v-model="user.role"
item-value="name"
:items="roles"
class="py-0 my-0"
@change="setUserPermissions(user)"
>
<template #selection="{ item }">
{{ item.name }}
</template>
<template #item="{ item }">
<div class="pa-2">
<p class="pa-0 ma-0">{{ item.name }}</p>
<p class="caption pa-0 ma-0 grey--text" style="max-width: 300px">
{{ item.description }}
</p>
</div>
</template>
</v-select>
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-btn icon small color="error" @click="removeUser(user)">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</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 UserAvatar from '@/components/UserAvatar'
import StreamInviteDialog from '@/components/dialogs/StreamInviteDialog'
export default {
components: { UserAvatar, StreamInviteDialog },
data: () => ({
search: '',
selectedUsers: null,
selectedRole: null,
userSearch: { items: [] },
serverInfo: { roles: [] },
loading: false
}),
apollo: {
stream: {
prefetch: true,
query: streamCollaboratorsQuery,
variables() {
return {
id: this.$route.params.streamId
}
}
},
userSearch: {
query: userSearchQuery,
variables() {
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()
},
collaborators() {
if (!this.stream) return []
return this.stream.collaborators.filter((user) => user.id !== this.myId)
},
filteredSearchResults() {
if (!this.userSearch) return null
let users = []
for (let u of this.userSearch.items) {
if (u.id === this.myId) continue
let indx = this.collaborators.findIndex((eu) => eu.id === u.id)
if (indx === -1) users.push(u)
}
return users
},
myId() {
return localStorage.getItem('uuid')
}
},
methods: {
async removeUser(user) {
this.loading = true
this.$matomo && this.$matomo.trackPageView('stream/remove-collaborator')
try {
await this.$apollo.mutate({
mutation: gql`
mutation streamRevokePermission($params: StreamRevokePermissionInput!) {
streamRevokePermission(permissionParams: $params)
}
`,
variables: {
params: {
streamId: this.stream.id,
userId: user.id
}
}
})
let index = this.stream.collaborators.findIndex((u) => u.id === user.id)
if (index !== -1) {
this.stream.collaborators.splice(index, 1)
}
} catch (e) {
console.log(e)
}
this.$apollo.queries.stream.refetch()
this.loading = false
},
async setUserPermissions(user) {
this.loading = true
await this.grantPermissionUser(user)
this.loading = false
this.$apollo.queries.stream.refetch()
},
async addCollab(user) {
this.loading = true
this.search = null
this.userSearch.items = null
user.role = 'stream:contributor'
await this.grantPermissionUser(user)
this.stream.collaborators.unshift(user)
this.loading = false
this.$apollo.queries.stream.refetch()
},
async grantPermissionUser(user) {
this.$matomo && this.$matomo.trackPageView('stream/add-collaborator')
try {
await this.$apollo.mutate({
mutation: gql`
mutation grantPerm($params: StreamGrantPermissionInput!) {
streamGrantPermission(permissionParams: $params)
}
`,
variables: {
params: {
streamId: this.stream.id,
userId: user.id,
role: user.role
}
}
})
} catch (e) {
console.log(e)
}
},
showStreamInviteDialog() {
this.$refs.streamInviteDialog.show()
}
}
}
</script>
@@ -13,22 +13,33 @@
<v-icon class="mr-2">mdi-earth</v-icon>
Globals
</v-card-title>
<v-card-subtitle v-if="commitMessage" class="mt-3">
<b>Selected commit:</b>
<v-icon dense class="text-subtitle-1">mdi-source-commit</v-icon>
{{ commitMessage }}
</v-card-subtitle>
<v-card-text>
These global variables can be used for storing design values, project requirements, notes, or
any info you want to keep track of alongside your geometry. Variable values can be text,
numbers, lists, or booleans. Click the box icon next to any field to turn it into a nested
group of fields, and drag and drop fields in and out of groups as you please! Note that field
order may not always be preserved.
</v-card-text>
<v-card-text v-if="!(userRole === 'stream:contributor') && !(userRole === 'stream:owner')">
You are free to play around with the globals here, but you do not have the required stream
permission to save your changes.
<v-card-text class="subtitle-1">
Click the box icon next to any field to turn it into a nested group of fields, and drag and
drop fields in and out of groups as you please! Note that field order may not always be
preserved.
<span v-if="!(userRole === 'stream:contributor') && !(userRole === 'stream:owner')">
<br />
<br />
You are free to play around with the globals here, but you do not have the required stream
permission to save your changes.
</span>
<v-btn
text
small
color="primary"
href="https://speckle.guide/user/web.html#globals"
target="_blank"
>
Read the docs
</v-btn>
<div v-if="commitMessage" class="mt-3">
<b>Selected commit:</b>
<v-icon dense class="text-subtitle-1">mdi-source-commit</v-icon>
{{ commitMessage }}
</div>
</v-card-text>
<v-card-actions>
<v-switch
v-model="deleteEntries"
@@ -45,6 +45,10 @@ export default {
streamId: {
type: String,
default: null
},
text: {
type: String,
default: ''
}
},
data() {
@@ -78,7 +82,7 @@ export default {
watch: {
showDialog() {
this.clear()
this.email = null
this.email = this.text
this.message = 'Hey, I want to share this stream with you!'
}
},
+9
View File
@@ -132,6 +132,15 @@ const routes = [
props: true,
component: () => import('@/views/stream/Activity.vue')
},
{
path: 'collaborators/',
name: 'collaborators',
meta: {
title: 'Stream Collaborators | Speckle'
},
props: true,
component: () => import('@/views/stream/Collaborators.vue')
},
{
path: 'settings/',
name: 'settings',
+43 -20
View File
@@ -76,26 +76,43 @@
</v-row>
</v-container>
</v-app-bar>
<v-navigation-drawer v-if="isStreamPage" v-model="drawer" app clipped left>
<v-list v-if="stream">
<v-navigation-drawer v-if="isStreamPage && stream" v-model="drawer" app clipped left>
<v-list>
<v-subheader>Stream menu</v-subheader>
<v-list-item
v-for="menu in menues"
:key="menu.name"
:to="menu.to"
:disabled="menu.disabled"
exact
@click="handleFunction(menu.click)"
>
<v-list-item-icon>
<v-icon>{{ menu.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-group color="primary">
<v-list-item
v-for="menu in menues"
:key="menu.name"
:to="menu.to"
:disabled="menu.disabled"
exact
@click="handleFunction(menu.click)"
>
<v-list-item-icon>
<v-icon>{{ menu.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ menu.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item-content>
<v-list-item-title>{{ menu.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
<v-btn
v-if="stream.role === 'stream:owner'"
outlined
color="primary"
rounded
height="50"
class="ma-3 d-block"
@click="showStreamInviteDialog"
>
<v-icon small class="mr-2">mdi-email-send-outline</v-icon>
Invite to this
<br />
stream by email
</v-btn>
<stream-invite-dialog ref="streamInviteDialog" :stream-id="stream.id" />
</v-navigation-drawer>
<v-main :style="background">
<router-view></router-view>
@@ -134,9 +151,10 @@ import gql from 'graphql-tag'
import userQuery from '../graphql/user.gql'
import UserMenuTop from '../components/UserMenuTop'
import SearchBar from '../components/SearchBar'
import StreamInviteDialog from '../components/dialogs/StreamInviteDialog'
export default {
components: { UserMenuTop, SearchBar },
components: { UserMenuTop, SearchBar, StreamInviteDialog },
data() {
return {
search: '',
@@ -247,7 +265,7 @@ export default {
{
name: 'Collaborators',
icon: 'mdi-account-group-outline',
click: 'manageCollabrators',
to: '/streams/' + this.$route.params.streamId + '/collaborators',
disabled: this.stream.role !== 'stream:owner'
},
{
@@ -259,7 +277,7 @@ export default {
{
name: 'Settings',
icon: 'mdi-cog-outline',
click: 'editStream',
to: '/streams/' + this.$route.params.streamId + '/settings',
disabled: this.stream.role !== 'stream:owner'
}
]
@@ -274,11 +292,16 @@ export default {
methods: {
handleFunction(f) {
if (this[f]) this[f]()
},
showStreamInviteDialog() {
this.$refs.streamInviteDialog.show()
}
}
}
</script>
<style>
.action-button {
}
.logo {
font-family: 'Space Grotesk', sans-serif;
text-transform: none;
@@ -2,6 +2,7 @@
<v-row>
<v-col v-if="stream" cols="12">
<breadcrumb-title />
<h3 class="title font-italic font-weight-thin my-5">Recent activity on this Stream</h3>
</v-col>
<v-col cols="12">
<v-timeline v-if="stream" align-top>
+8 -15
View File
@@ -6,35 +6,28 @@
</v-col>
<v-col v-else-if="stream.branch" cols="12">
<breadcrumb-title />
<v-card class="pa-4" elevation="0" rounded="lg">
<h3 v-if="stream.branch.description" class="title font-italic font-weight-thin my-5">
{{ stream.branch.descrption }}
</h3>
<v-card class="mt-5 pa-4" elevation="0" rounded="lg">
<branch-edit-dialog ref="editBranchDialog" />
<v-card-title class="mr-8">
<v-card-title>
<v-icon class="mr-2">mdi-source-branch</v-icon>
<span class="d-inline-block">{{ stream.branch.name }}</span>
<v-spacer />
<v-btn
v-if="stream.role === 'stream:contributor' || stream.role === 'stream:owner'"
small
color="primary"
text
class="px-0"
class="my-2"
small
@click="editBranch"
>
<v-icon small class="mr-2 float-left">mdi-cog-outline</v-icon>
Edit branch
</v-btn>
</v-card-title>
<v-card-text v-if="stream.branch.description">
{{ stream.branch.description }}
</v-card-text>
<v-card-text v-else>
<i>No description provided</i>
</v-card-text>
</v-card>
<v-card class="mt-5 pa-4" elevation="0" rounded="lg">
<v-subheader class="text-uppercase">
Commits ({{ stream.branch.commits.totalCount }})
</v-subheader>
@@ -3,38 +3,30 @@
<v-col v-if="$apollo.queries.stream.loading">
<v-skeleton-loader type="article"></v-skeleton-loader>
</v-col>
<v-col v-else-if="branches" cols="12">
<v-col v-else-if="stream" cols="12">
<breadcrumb-title />
<v-card rounded="lg" class="pa-4 mb-4" elevation="0">
<branch-new-dialog ref="newBranchDialog" />
<h3 class="title font-italic font-weight-thin my-5">
A branch represents a series of commits, you can see them as labels, folders etc
</h3>
<v-card class="mt-5 pa-4" elevation="0" rounded="lg">
<v-card-title>
<v-icon class="mr-2">mdi-source-branch</v-icon>
Branches
<span class="d-inline-block">Branches ({{ branches.length }})</span>
<v-spacer />
<v-btn
v-if="stream.role === 'stream:contributor' || stream.role === 'stream:owner'"
color="primary"
text
class="px-0"
class="my-2"
small
@click="newBranch"
>
<v-icon small class="mr-2 float-left">mdi-plus-circle-outline</v-icon>
<!-- <v-icon small class="mr-2 float-left">mdi-plus-circle-outline</v-icon> -->
New branch
</v-btn>
<branch-new-dialog ref="newBranchDialog" />
</v-card-title>
<v-card-text>
<i>
A branch represents an independent line of data. You can think of them as an independent
directory, staging area and project history.
</i>
</v-card-text>
</v-card>
<v-card v-if="!$apollo.queries.stream.loading" class="mt-5 pa-4" elevation="0" rounded="lg">
<v-subheader class="text-uppercase">Branches ({{ branches.length }})</v-subheader>
<v-card-text>
<v-list two-line color="transparent">
<template v-for="item in branches">
@@ -0,0 +1,22 @@
<template>
<v-row>
<v-col cols="12">
<breadcrumb-title />
<h3 class="title font-italic font-weight-thin my-5">Manage who has access to this Stream</h3>
<collaborators-manage />
</v-col>
</v-row>
</template>
<script>
export default {
name: 'Collaborators',
components: {
BreadcrumbTitle: () => import('@/components/BreadcrumbTitle'),
CollaboratorsManage: () => import('@/components/CollaboratorsManage')
},
data() {
return {}
}
}
</script>
+32 -20
View File
@@ -2,37 +2,49 @@
<v-row>
<v-col>
<breadcrumb-title />
<h3 class="title font-italic font-weight-thin my-5">
Globals store design values, project requirements, notes etc
</h3>
<div v-if="!objectId && !$apollo.loading && !revealBuilder">
<v-card :loading="loading">
<v-card :loading="loading" class="mt-5 pa-4" elevation="0" rounded="lg">
<template slot="progress">
<v-progress-linear indeterminate></v-progress-linear>
</template>
<v-card-title>You don't have any globals on this stream!</v-card-title>
<v-card-text
v-if="stream.role === 'stream:contributor' || stream.role === 'stream:owner'"
class="subtitle-1"
>
Globals are useful for storing design values, project requirements, notes, or any info
you want to keep track of alongside your geometry. Would you like to create some now?
</v-card-text>
<v-card-text
v-if="!(stream.role === 'contributor') && !(stream.role === 'stream:owner')"
class="subtitle-1"
>
Globals are useful for storing design values, project requirements, notes, or any info
you want to keep track of alongside your geometry. You don't have permission to create
and edit globals on this stream, but you can create your own stream to try them out!
</v-card-text>
<v-card-actions>
<v-card-title>
<v-icon class="mr-2">mdi-earth</v-icon>
Globals
<v-spacer />
<v-btn
v-if="stream.role === 'stream:contributor' || stream.role === 'stream:owner'"
color="primary"
dark
class="ma-2"
small
@click="createClicked"
>
create globals
</v-btn>
</v-card-actions>
</v-card-title>
<v-card-text class="subtitle-1">
There are no globals in this stream yet.
<br />
Globals are useful for storing design values, project requirements, notes, or any info
you want to keep track of alongside your geometry.
<strong v-if="!(stream.role === 'contributor') && !(stream.role === 'stream:owner')">
<br />
<br />
You don't have permission to create globals in this stream.
</strong>
<v-btn
text
small
color="primary"
href="https://speckle.guide/user/web.html#globals"
target="_blank"
>
Read the docs
</v-btn>
</v-card-text>
</v-card>
</div>
<div v-if="objectId || revealBuilder">
@@ -52,7 +64,7 @@
>
<v-card-title>
<v-icon class="mr-2">mdi-history</v-icon>
History
Globals History
</v-card-title>
<v-card-text>
<list-item-commit
+20 -18
View File
@@ -1,10 +1,20 @@
<template>
<v-row v-if="!error">
<v-col v-if="stream" cols="12">
<breadcrumb-title />
<h1 class="display-1">{{ stream.name }}</h1>
<h3 class="title font-italic font-weight-thin my-5">
{{ truncate(stream.description) }}
</h3>
<div class="mb-3">
<span class="caption">
Created
<timeago v-tooltip="formatDate(stream.createdAt)" :datetime="stream.createdAt"></timeago>
</span>
<span class="ml-3 caption">
Updated
<timeago v-tooltip="formatDate(stream.updatedAt)" :datetime="stream.updatedAt"></timeago>
</span>
</div>
<div>
<v-chip>
<v-icon small>mdi-source-branch</v-icon>
@@ -32,14 +42,15 @@
&nbsp; private
</span>
</v-chip>
<span class="ml-3 caption">
Created
<timeago v-tooltip="formatDate(stream.createdAt)" :datetime="stream.createdAt"></timeago>
</span>
<span class="ml-3 caption">
Updated
<timeago v-tooltip="formatDate(stream.updatedAt)" :datetime="stream.updatedAt"></timeago>
</span>
<user-avatar
v-for="(collab, i) in stream.collaborators"
:id="collab.id"
:key="i"
:size="30"
:avatar="collab.avatar"
:name="collab.name"
class="ml-1"
></user-avatar>
</div>
</v-col>
@@ -103,16 +114,7 @@
See commit details
</v-btn>
</v-sheet>
<h3 class="title mt-4 mb-3">Collaborators</h3>
<user-avatar
v-for="(collab, i) in stream.collaborators"
:id="collab.id"
:key="i"
:size="40"
:avatar="collab.avatar"
:name="collab.name"
></user-avatar>
<no-data-placeholder v-if="!latestCommit" :message="`This branch has no commits.`" />
</v-card>
</div>
+74 -68
View File
@@ -1,86 +1,86 @@
<template>
<admin-card :loading="loading" title="General">
<v-card-text class="py-0 my-0">
<v-form ref="form" v-model="valid" class="px-2" @submit.prevent="save">
<v-text-field
v-model="name"
:rules="validation.nameRules"
label="Name"
hint="The name of this stream."
/>
<p class="subtitle-1">Description</p>
<v-row>
<v-col cols="12" sm="12" md="6">
<p class="caption">
Use Markdown! Tips:
<code>#, ##, ###</code>
prefix headings, links:
<code>[speckle](https://speckle.systems)</code>
, images:
<code>![image title](image url)</code>
, list items are prefixed by
<code>-</code>
on new lines,
<b>bold</b>
text by surrounding it with
<code>**</code>
, etc.
</p>
<v-textarea
<v-row>
<v-col v-if="stream" cols="12">
<breadcrumb-title />
<h3 class="title font-italic font-weight-thin my-5">Fine tune this Stream's settings</h3>
<v-card class="mt-5 pa-4" elevation="0" rounded="lg" :loading="loading">
<v-card-title>
<v-icon class="mr-2">mdi-cog</v-icon>
<span class="d-inline-block">General</span>
</v-card-title>
<v-card-text>
<v-form ref="form" v-model="valid" class="px-2" @submit.prevent="save">
<v-text-field
v-model="name"
:rules="validation.nameRules"
label="Name"
hint="The name of this stream."
class="mt-5"
/>
<v-text-field
v-model="description"
auto-grow
filled
rows="10"
style="font-size: 12px; line-height: 10px"
></v-textarea>
</v-col>
<v-col cols="12" sm="12" md="6">
<p class="subtitle">Preview</p>
<div class="marked-preview" v-html="compiledMarkdown"></div>
</v-col>
</v-row>
<v-switch
v-model="isPublic"
:label="isPublic ? 'Public' : 'Private'"
:hint="
isPublic
? 'Anyone can view this stream. It is also visible on your profile page. Only collaborators can edit it.'
: 'Only collaborators can access this stream.'
"
persistent-hint
/>
</v-form>
</v-card-text>
label="Description"
hint="The description of this stream."
class="mt-5"
/>
<v-divider class="mt-4 mb-3" />
<v-switch
v-model="isPublic"
class="mt-5"
:label="isPublic ? 'Public' : 'Private'"
:hint="
isPublic
? 'Anyone can view this stream. It is also visible on your profile page. Only collaborators can edit it.'
: 'Only collaborators can access this stream.'
"
persistent-hint
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn outlined color="success" type="submit" :disabled="!valid" @click="save">
Save Changes
</v-btn>
</v-card-actions>
</admin-card>
<v-card-actions>
<v-btn class="ml-3 mt-5" color="primary" type="submit" :disabled="!canSave" @click="save">
Save Changes
</v-btn>
</v-card-actions>
</v-card>
</v-col>
<v-snackbar v-model="snackbar" timeout="800" color="primary">
<p class="text-center my-0">
<b>Changes saved!</b>
</p>
</v-snackbar>
</v-row>
</template>
<script>
import marked from 'marked'
import DOMPurify from 'dompurify'
import gql from 'graphql-tag'
import streamQuery from '@/graphql/stream.gql'
export default {
name: 'SettingsGeneral',
components: {
AdminCard: () => import('@/components/admin/AdminCard')
BreadcrumbTitle: () => import('@/components/BreadcrumbTitle')
},
apollo: {
stream: {
query: streamQuery,
query: gql`
query Stream($id: String!) {
stream(id: $id) {
id
name
description
isPublic
}
}
`,
variables() {
return {
id: this.$attrs.streamId
id: this.$route.params.streamId
}
},
update(data) {
let stream = data.stream
if (stream)
@@ -91,6 +91,7 @@ export default {
}
},
data: () => ({
snackbar: false,
loading: false,
valid: false,
name: null,
@@ -101,12 +102,16 @@ export default {
}
}),
computed: {
compiledMarkdown() {
if (!this.description) return ''
let md = marked(this.description)
return DOMPurify.sanitize(md)
canSave() {
return (
this.valid &&
(this.name !== this.stream.name ||
this.description !== this.stream.description ||
this.isPublic !== this.stream.isPublic)
)
}
},
methods: {
async save() {
this.loading = true
@@ -127,6 +132,7 @@ export default {
}
}
})
this.snackbar = true
} catch (e) {
console.log(e)
}
@@ -2,6 +2,9 @@
<v-row>
<v-col>
<breadcrumb-title />
<h3 class="title font-italic font-weight-thin my-5">
Automate anything by adding webhooks to Stream events
</h3>
<admin-card v-if="selectedWebhook != undefined" :loading="loading" title="Edit Webhook">
<template #subtitle>
<v-icon dense class="text-subtitle-1 pr-1">mdi-webhook</v-icon>
@@ -30,14 +33,28 @@
<admin-card v-else title="Webhooks" icon="mdi-webhook">
<template #menu>
<v-btn small outlined color="primary" :to="`/streams/${$attrs.streamId}/webhooks/new`">
<v-btn
color="primary"
dark
class="ma-2"
small
:to="`/streams/${$attrs.streamId}/webhooks/new`"
>
Add Webhook
</v-btn>
</template>
<v-card-text v-if="webhooks && webhooks.length == 0">
You don't have any webhooks on this stream yet. Click the blue "Add Webhook" button in the
top right to add one.
There are no webhooks on this stream yet.
<v-btn
text
small
color="primary"
href="https://speckle.guide/dev/server-webhooks.html"
target="_blank"
>
Read the docs
</v-btn>
</v-card-text>
<v-list subheader two-line>