feat: apollo client v3 + vue apollo v4 (#831)

This commit is contained in:
Kristaps Fabians Geikins
2022-07-25 12:24:59 +03:00
committed by GitHub
parent 79f558c726
commit b55f12d6bc
110 changed files with 1582 additions and 4875 deletions
+7
View File
@@ -36,6 +36,13 @@ const config = {
commonjs: true
}
},
{
files: './build-config/**/*.{js, ts}',
env: {
node: true,
commonjs: true
}
},
{
files: '*.ts',
plugins: ['@typescript-eslint'],
+13 -1
View File
@@ -38,6 +38,12 @@ Build static build & serve it (for development, otherwise use docker image):
yarn build && yarn serve
```
### Apollo Client
We're on Apollo Client v3 and Vue Apollo v4 (both the options API and composition API) in this package, so pretty much all of the latest and greatest features are there and ready to be used.
**Note**: Do not import anything from `@apollo/client`, use `@apollo/client/core` instead! Otherwise you risk bundling in React dependencies, which we definitely do not need!
### TypeScript
This project also supports TypeScript, both in Vue SFCs and outside them. It's preferred that you use it when writing new code and also migrate JS files when there's a good oppurtunity to do so.
@@ -52,7 +58,7 @@ Note: If you're defining a Vue component in a non-standard way (e.g. `vueWithMix
#### Improved GraphQL DX w/ TS
Run `yarn gqlgen` to generate relevant TS types from the GraphQL Schema (introspected from server which must be running) and operations defined in the frontend. Check this out for more info: https://www.graphql-code-generator.com/plugins/typescript-vue-apollo-smart-ops#examples
Run `yarn gqlgen` to generate relevant TS types from the GraphQL Schema (introspected from server which must be running) and operations defined in the frontend. Afterwards make sure you import them from the generated `graphql.ts` file, not the original file where you defined the operation/fragment.
### Packaging for production
@@ -68,6 +74,12 @@ Restart the Vetur Vue Language Server (VLS) through the command palette. Vetur i
If you are getting a lot of Property 'xxx' does not exist on type 'CombinedVueInstance' errors, it's an issue with Vue's typing and TypeScript. You can work around it by annotating the return type for each computed/data property, making sure data/props keys are defined even if they're empty.
#### Vue Apollo Options API fetchMore() doesn't update the state/template quickly enough
This seems to be an issue that appeared after the Apollo Client v2 -> Apollo Client v3 upgrade. It only becomes an issue when you're trying to use vue-infinite-loader with fetchMore(), because the infinite loader triggers an infinite amount of requests with the old cursor, because the cursor hasn't been updated yet at that point.
The workaround is simple - use the Vue Apollo Composition API fetchMore
## Community
If in trouble, the Speckle Community hangs out on [the forum](https://speckle.community). Do join and introduce yourself! We're happy to help.
+2 -18
View File
@@ -1,23 +1,7 @@
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')
])
module.exports = {
client: {
service: env.VUE_APP_APOLLO_ENGINE_SERVICE,
service: 'speckle-server',
url: 'http://localhost:3000/graphql',
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')
},
engine: {
endpoint: process.env.APOLLO_ENGINE_API_ENDPOINT,
apiKey: env.VUE_APP_APOLLO_ENGINE_KEY
}
}
@@ -0,0 +1,27 @@
const TARGET = 'es2019'
/**
* GQL file support (previously this was managed by the vue apollo cli plugin)
* @param {import('@vue/cli-service/lib/PluginAPI')} api
**/
function plugin(api) {
api.chainWebpack((config) => {
const gqlRule = config.module.rule('gql').test(/\.(gql|graphql)$/)
// add caching
gqlRule
.use('cache-loader')
.loader('cache-loader')
.options(
api.genCacheConfig('gql-cache-loader', {
target: TARGET,
graphqltagVersion: require('graphql-tag/package.json').version
})
)
// add gql loader
gqlRule.use('gql-loader').loader('graphql-tag/loader')
})
}
module.exports = plugin
+1 -3
View File
@@ -9,9 +9,7 @@ generates:
- 'typescript'
- 'typescript-operations'
- 'typescript-document-nodes'
- 'typescript-vue-apollo-smart-ops'
- 'typed-document-node'
config:
vueApolloErrorHandlerFunction: handleApolloError
vueApolloErrorHandlerFunctionImportFrom: '@/config/vueApolloSmartOpsConfig'
scalars:
JSONObject: Record<string, unknown>
+11 -6
View File
@@ -17,6 +17,7 @@
"gqlgen": "graphql-codegen --config codegen.yml"
},
"dependencies": {
"@apollo/client": "^3.6.9",
"@speckle/viewer": "workspace:^",
"@tiptap/core": "^2.0.0-beta.176",
"@tiptap/extension-bold": "^2.0.0-beta.26",
@@ -33,20 +34,25 @@
"@tiptap/extension-underline": "^2.0.0-beta.23",
"@tiptap/vue-2": "^2.0.0-beta.79",
"@tryghost/content-api": "^1.5.12",
"@vue/apollo-composable": "^4.0.0-alpha.19",
"@vue/apollo-option": "^4.0.0-alpha.20",
"@vuejs-community/vue-filter-date-format": "^1.6.3",
"@vuejs-community/vue-filter-date-parse": "^1.1.6",
"apexcharts": "^3.33.1",
"apollo-upload-client": "^17.0.0",
"dompurify": "^2.3.6",
"graphql": "^15.0.0",
"graphql-tag": "^2.12.6",
"lodash": "^4.17.21",
"numeral": "^2.0.6",
"portal-vue": "^2.1.7",
"regenerator-runtime": "^0.13.9",
"subscriptions-transport-ws": "^0.11.0",
"tween": "^0.9.0",
"uuid": "^8.3.2",
"v-tooltip": "^2.0.3",
"vue": "^2.7.5",
"vue-apexcharts": "^1.6.1",
"vue-apollo": "^3.0.5",
"vue-histogram-slider": "^0.3.8",
"vue-infinite-loading": "^2.4.5",
"vue-mixpanel": "1.0.7",
@@ -61,12 +67,13 @@
"devDependencies": {
"@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typed-document-node": "^2.3.1",
"@graphql-codegen/typescript": "2.5.1",
"@graphql-codegen/typescript-document-nodes": "2.2.13",
"@graphql-codegen/typescript-operations": "2.4.2",
"@graphql-codegen/typescript-vue-apollo-smart-ops": "^2.3.1",
"@mdi/font": "^5.8.55",
"@rushstack/eslint-patch": "^1.1.3",
"@types/apollo-upload-client": "^17.0.1",
"@types/dompurify": "^2.3.3",
"@types/lodash": "^4.14.180",
"@types/mixpanel-browser": "^2.38.0",
@@ -88,7 +95,6 @@
"eslint-config-prettier": "^8.5.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-vue": "^9.2.0",
"graphql-tag": "^2.11.0",
"local-web-server": "^5.2.0",
"lodash-webpack-plugin": "^0.11.6",
"prettier": "^2.5.1",
@@ -98,8 +104,6 @@
"type-fest": "^2.13.1",
"typescript": "~4.1.5",
"vti": "^0.1.5",
"vue-apollo-smart-ops": "^0.2.0-beta.1",
"vue-cli-plugin-apollo": "~0.22.2",
"vue-cli-plugin-vuetify": "^2.5.1",
"vuetify-loader": "^1.9.1",
"webpack": "^4.46.0",
@@ -110,7 +114,8 @@
},
"vuePlugins": {
"service": [
"./esbuildPlugin.js"
"./build-config/esbuildPlugin.js",
"./build-config/gqlPlugin.js"
]
}
}
-3
View File
@@ -1,6 +1,3 @@
// @see https://github.com/Akryum/vue-cli-plugin-apollo/issues/452
import 'regenerator-runtime/runtime'
import Vue from 'vue'
import VTooltip from 'v-tooltip'
import VueMixpanel from 'vue-mixpanel'
@@ -0,0 +1,222 @@
import Vue from 'vue'
import { createApolloProvider, ApolloProvider } from '@vue/apollo-option'
import { ApolloClient, ApolloLink, InMemoryCache, split } from '@apollo/client/core'
import { setContext } from '@apollo/client/link/context'
import { WebSocketLink } from '@apollo/client/link/ws'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import { LocalStorageKeys } from '@/helpers/mainConstants'
import { createUploadLink } from 'apollo-upload-client'
import { AppLocalStorage } from '@/utils/localStorage'
import { getMainDefinition } from '@apollo/client/utilities'
import { OperationDefinitionNode, Kind } from 'graphql'
import {
buildAbstractCollectionMergeFunction,
incomingOverwritesExistingMergeFunction
} from '@/main/lib/core/helpers/apolloSetupHelper'
// Name of the localStorage item
const AUTH_TOKEN = LocalStorageKeys.AuthToken
// Http endpoint
const httpEndpoint = `${window.location.origin}/graphql`
// WS endpoint
const wsEndpoint = `${window.location.origin.replace('http', 'ws')}/graphql`
// app version
const appVersion = process.env.SPECKLE_SERVER_VERSION || 'unknown'
function hasAuthToken() {
return !!AppLocalStorage.get(AUTH_TOKEN)
}
function createCache(): InMemoryCache {
return new InMemoryCache({
/**
* This is where you configure how various GQL fields should be read, written to or merged when new data comes in.
* If you define a merge function here, you don't need to duplicate the merge logic inside an `update()` callback
* of a fetchMore call, for example.
*
* Feel free to re-use utilities in `apolloSetupHelper` for defining merge functions or even use the ones that come from `@apollo/client/utilities`.
*
* Read more: https://www.apollographql.com/docs/react/caching/cache-field-behavior
*/
typePolicies: {
Query: {
fields: {
user: {
read(original, { args, toReference }) {
if (args?.id) {
return toReference({ __typename: 'User', id: args.id })
}
return original
}
},
stream: {
read(original, { args, toReference }) {
if (args?.id) {
return toReference({ __typename: 'Stream', id: args.id })
}
return original
}
},
streams: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('StreamCollection', {
checkIdentity: true
})
}
}
},
User: {
fields: {
timeline: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
},
commits: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('CommitCollectionUser', {
checkIdentity: true
})
},
favoriteStreams: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('StreamCollection', {
checkIdentity: true
})
}
}
},
Stream: {
fields: {
activity: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
},
commits: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
checkIdentity: true
})
},
pendingCollaborators: {
merge: incomingOverwritesExistingMergeFunction
}
}
},
Branch: {
fields: {
commits: {
keyArgs: false,
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
checkIdentity: true
})
}
}
},
BranchCollection: {
merge: true
},
ServerStats: {
merge: true
},
WebhookEventCollection: {
merge: true
},
ServerInfo: {
merge: true
},
CommentThreadActivityMessage: {
merge: true
}
}
})
}
function createWsClient(): SubscriptionClient {
return new SubscriptionClient(wsEndpoint, {
reconnect: true,
connectionParams: () => {
const authToken = AppLocalStorage.get(AUTH_TOKEN)
const Authorization = authToken ? `Bearer ${authToken}` : null
return Authorization ? { Authorization, headers: { Authorization } } : {}
}
})
}
function createLink(wsClient?: SubscriptionClient): ApolloLink {
// Prepare links
const httpLink = createUploadLink({
uri: httpEndpoint
})
const authLink = setContext(async (_, { headers }) => {
const authToken = AppLocalStorage.get(AUTH_TOKEN)
const authHeader = authToken ? { Authorization: `Bearer ${authToken}` } : {}
return {
headers: {
...headers,
...authHeader
}
}
})
let link = authLink.concat(httpLink)
if (wsClient) {
const wsLink = new WebSocketLink(wsClient)
link = split(
({ query }) => {
const definition = getMainDefinition(query) as OperationDefinitionNode
const { kind, operation } = definition
return kind === Kind.OPERATION_DEFINITION && operation === 'subscription'
},
wsLink,
link
)
}
return link
}
function createApolloClient() {
const cache = createCache()
const wsClient = createWsClient()
const link = createLink(wsClient)
const apolloClient = new ApolloClient({
link,
cache,
ssrForceFetchDelay: 100,
connectToDevTools: process.env.NODE_ENV !== 'production',
name: 'web',
version: appVersion
})
return {
apolloClient,
wsClient
}
}
/**
* Create a Vue Apollo provider instance
*/
export function createProvider(): ApolloProvider {
// Create apollo client
const { apolloClient, wsClient } = createApolloClient()
apolloClient.wsClient = hasAuthToken() ? wsClient : null
// Create vue apollo provider
const apolloProvider = createApolloProvider({
defaultClient: apolloClient
})
return apolloProvider
}
export function installVueApollo(apolloProvider: ApolloProvider): void {
// Install apollo provider (it's done weirdly cause it's meant to be used with vue 3)
Vue.config.globalProperties ||= {}
Vue.prototype.$apolloProvider = apolloProvider
apolloProvider.install(Vue)
}
@@ -1,35 +0,0 @@
import {
ApolloErrorType,
ApolloOperationErrorHandlerFunction,
ProcessedApolloError
} from 'vue-apollo-smart-ops'
/**
* Error handler used in our auto-generated graphql operation functions
*/
export const handleApolloError: ApolloOperationErrorHandlerFunction = (error) => {
const allErrors: ProcessedApolloError[] = []
if (error.networkError) {
const networkError: ProcessedApolloError = {
type: ApolloErrorType.NETWORK_ERROR,
error: error.networkError,
message: error.message
}
allErrors.push(networkError)
} else {
for (const gqlError of error.graphQLErrors || []) {
const basicError: ProcessedApolloError = {
type: ApolloErrorType.SERVER_ERROR,
error: gqlError,
path: gqlError.path,
message: gqlError.message
}
allErrors.push(basicError)
}
}
return {
allErrors
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export const branchCreatedSubscription = gql`
subscription BranchCreated($streamId: String!) {
+1 -1
View File
@@ -1,4 +1,4 @@
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export const COMMENT_FULL_INFO_FRAGMENT = gql`
fragment CommentFullInfo on Comment {
@@ -1,4 +1,4 @@
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export const limitedUserFieldsFragment = gql`
fragment LimitedUserFields on LimitedUser {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { usersOwnInviteFieldsFragment } from '@/graphql/fragments/user'
export const streamInviteQuery = gql`
+1 -1
View File
@@ -1,4 +1,4 @@
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export const mainServerInfoFieldsFragment = gql`
fragment MainServerInfoFields on ServerInfo {
+31 -1
View File
@@ -2,7 +2,7 @@ import {
limitedUserFieldsFragment,
streamCollaboratorFieldsFragment
} from '@/graphql/fragments/user'
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
/**
* Common stream fields when querying for streams
@@ -75,6 +75,36 @@ export const streamWithCollaboratorsQuery = gql`
${streamCollaboratorFieldsFragment}
`
export const streamWithActivityQuery = gql`
query StreamWithActivity($id: String!, $cursor: DateTime) {
stream(id: $id) {
id
name
createdAt
commits {
totalCount
}
branches {
totalCount
}
activity(cursor: $cursor) {
totalCount
cursor
items {
actionType
userId
streamId
resourceId
resourceType
time
info
message
}
}
}
}
`
/**
* Remove authenticated user from the collaborators list
*/
+23 -1
View File
@@ -1,6 +1,6 @@
import { limitedUserFieldsFragment } from '@/graphql/fragments/user'
import { commonStreamFieldsFragment } from '@/graphql/streams'
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export const commonUserFieldsFragment = gql`
fragment CommonUserFields on User {
@@ -139,3 +139,25 @@ export const adminUsersListQuery = gql`
}
}
`
export const userTimelineQuery = gql`
query UserTimeline($cursor: DateTime) {
user {
id
timeline(cursor: $cursor) {
totalCount
cursor
items {
actionType
userId
streamId
resourceId
resourceType
time
info
message
}
}
}
}
`
@@ -1,49 +0,0 @@
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
}
}
}
}
+1 -10
View File
@@ -2,16 +2,7 @@
<router-view></router-view>
</template>
<script>
import { mainServerInfoQuery } from '@/graphql/server'
export default {
components: {},
apollo: {
serverInfo: {
query: mainServerInfoQuery
}
}
}
export default {}
</script>
<style lang="css">
.v-timeline:before {
+13 -6
View File
@@ -6,7 +6,9 @@ import store from '@/main/store'
import { LocalStorageKeys } from '@/helpers/mainConstants'
import * as MixpanelManager from '@/mixpanelManager'
import { createProvider } from '@/vue-apollo'
import { provide } from 'vue'
import { DefaultApolloClient } from '@vue/apollo-composable'
import { createProvider, installVueApollo } from '@/config/apolloConfig'
import {
checkAccessCodeAndGetTokens,
prefetchUserAndSetSuuid
@@ -26,6 +28,8 @@ Vue.use(VueFilterDateFormat)
import PerfectScrollbar from 'vue2-perfect-scrollbar'
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'
// adds various helper methods
import '@/plugins/helpers'
Vue.use(PerfectScrollbar)
@@ -46,13 +50,13 @@ Vue.filter('capitalize', (value) => {
return value.charAt(0).toUpperCase() + value.slice(1)
})
// adds various helper methods
import '@/plugins/helpers'
const AuthToken = localStorage.getItem(LocalStorageKeys.AuthToken)
const RefreshToken = localStorage.getItem(LocalStorageKeys.RefreshToken)
const apolloProvider = createProvider()
const apolloProvider = createProvider()
installVueApollo(apolloProvider)
// TODO: Sort out error handling here, if something goes wrong it just goes into an infinite loop
if (AuthToken) {
prefetchUserAndSetSuuid(apolloProvider.defaultClient)
.then(() => {
@@ -62,6 +66,7 @@ if (AuthToken) {
if (RefreshToken) {
// TODO: try to rotate token & prefetch user, etc.
}
window.location = `${window.location.origin}/authn/login`
})
} else {
@@ -88,7 +93,9 @@ function postAuthInit() {
router,
vuetify,
store,
apolloProvider,
setup() {
provide(DefaultApolloClient, apolloProvider.defaultClient)
},
render: (h) => h(App)
}).$mount('#app')
}
@@ -284,7 +284,7 @@ import UserAvatar from '@/main/components/common/UserAvatar'
import UserPill from '@/main/components/activity/UserPill'
import SourceAppAvatar from '@/main/components/common/SourceAppAvatar'
import PreviewImage from '@/main/components/common/PreviewImage'
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import ListItemActivityDescription from '@/main/components/activity/ListItemActivityDescription.vue'
export default {
@@ -16,7 +16,7 @@
</v-chip>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import UserAvatar from '@/main/components/common/UserAvatar'
export default {
@@ -19,9 +19,11 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { formatNumber } from '@/plugins/formatNumber.js'
const EXCLUDED_SERVER_STATS_KEYS = ['__typename']
export default {
name: 'ActivityCard',
components: {
@@ -134,12 +136,7 @@ export default {
streamHistory
}
}
`,
update(data) {
const stats = data.serverStats
delete stats.__typename
return stats
}
`
}
},
computed: {
@@ -147,7 +144,10 @@ export default {
let result = []
const months = this.past12Months()
if (this.serverStats) {
result = Object.keys(this.serverStats).map((key) => {
const statsKeys = Object.keys(this.serverStats).filter(
(k) => !EXCLUDED_SERVER_STATS_KEYS.includes(k)
)
result = statsKeys.map((key) => {
const category = this.serverStats[key]
const processed = []
months?.forEach((month) => {
@@ -29,7 +29,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
name: 'GeneralInfoCard',
@@ -38,7 +38,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
name: 'VersionInfoCard',
@@ -115,7 +115,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { documentToBasicString } from '@/main/lib/common/text-editor/documentHelper'
import { COMMENT_FULL_INFO_FRAGMENT } from '@/graphql/comments'
@@ -178,6 +178,9 @@ export default {
},
result({ data }) {
if (!data || !data.commentThreadActivity) return
// Note: This kind of direct apollo result mutation is only allowed, because
// of the 'no-cache' fetch policy, which means that there's no cache mutation actually happening
if (data.commentThreadActivity.type === 'reply-added') {
this.commentDetails.replies.totalCount++
this.commentDetails.updatedAt = Date.now()
@@ -79,7 +79,7 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import SmartTextEditor from '@/main/components/common/text-editor/SmartTextEditor.vue'
import { SMART_EDITOR_SCHEMA } from '@/main/lib/viewer/comments/commentsHelper'
import CommentThreadReplyAttachments from '@/main/components/comments/CommentThreadReplyAttachments.vue'
@@ -377,7 +377,7 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import debounce from 'lodash/debounce'
import CommentThreadReply from '@/main/components/comments/CommentThreadReply.vue'
import CommentEditor from '@/main/components/comments/CommentEditor.vue'
@@ -77,7 +77,7 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
components: {
@@ -138,7 +138,7 @@
</v-container>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
props: {
showImage: {
@@ -4,7 +4,7 @@
<v-autocomplete
v-model="selectedSearchResult"
:loading="$apollo.loading"
:items="streams.items"
:items="items"
:search-input.sync="search"
no-filter
counter="3"
@@ -78,7 +78,7 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
components: {
@@ -91,7 +91,7 @@ export default {
search: '',
hasSearched: false,
liff: false,
streams: { items: [] },
items: [],
selectedSearchResult: null
}),
apollo: {
@@ -117,15 +117,18 @@ export default {
skip() {
return !this.search || this.search.length < 3
},
result({ data }) {
this.items = [...data.streams.items]
},
debounce: 300
}
},
watch: {
selectedSearchResult(val) {
const myStream = this.streams.items.find((s) => s.id === val.id)
const myStream = this.items.find((s) => s.id === val.id)
this.$emit('select', myStream)
this.streams.items = []
this.items = []
this.search = ''
if (val && this.gotostreamonclick) this.$router.push(`/streams/${val.id}`)
@@ -133,7 +136,7 @@ export default {
search(val) {
this.hasSearched = true
if (val === '42') this.liff = true
if (!val || val === '') this.streams.items = []
if (!val || val === '') this.items = []
}
},
methods: {}
@@ -1,6 +1,6 @@
<template>
<v-avatar :size="size" color="grey lighten-3">
<v-img v-if="avatar" :src="avatar" />
<v-img v-if="hasValidAvatar" :src="avatar" />
<v-img v-else :src="`https://robohash.org/${seed}.png?size=${size}x${size}`" />
</v-avatar>
</template>
@@ -20,6 +20,18 @@ export default {
type: String,
default: null
}
},
computed: {
hasValidAvatar() {
if (!this.avatar) return false
const validPrefixes = ['http', 'data:']
for (const validPrefix of validPrefixes) {
if (this.avatar.startsWith(validPrefix)) return true
}
return false
}
}
}
</script>
@@ -2,7 +2,7 @@
<div>
<user-stream-invite-banners @invite-used="onInviteUsed" />
<v-row dense>
<v-col v-if="$apollo.loading && !timeline">
<v-col v-if="isApolloLoading && !timeline">
<div class="my-5">
<v-timeline align-top dense>
<v-timeline-item v-for="i in 6" :key="i" medium>
@@ -73,11 +73,14 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import UserStreamInviteBanners from '@/main/components/stream/UserStreamInviteBanners.vue'
import InfiniteLoading from 'vue-infinite-loading'
import NoDataPlaceholder from '@/main/components/common/NoDataPlaceholder.vue'
import ListItemActivity from '@/main/components/activity/ListItemActivity.vue'
import { UserTimelineDocument } from '@/graphql/generated/graphql'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
export default {
name: 'FeedTimeline',
@@ -87,77 +90,26 @@ export default {
NoDataPlaceholder,
UserStreamInviteBanners
},
data() {
return {
newStreamDialog: 0,
activityNav: true
}
},
apollo: {
quickUser: {
query: gql`
query {
quickUser: user {
id
name
}
}
`
},
timeline: {
query: gql`
query ($cursor: DateTime) {
user {
id
timeline(cursor: $cursor) {
totalCount
cursor
items {
actionType
userId
streamId
resourceId
resourceType
time
info
message
}
}
}
}
`,
fetchPolicy: 'cache-and-network',
update(data) {
return data.user.timeline
setup() {
// Timeline query
const {
result: timelineResult,
fetchMore: timelineFetchMore,
refetch: timelineRefetch,
loading: timelineLoading
} = useQuery(
UserTimelineDocument,
{
cursor: null
},
result({ data }) {
this.groupSimilarActivities(data)
}
}
},
computed: {},
watch: {
timeline(val) {
if (val.totalCount === 0 && !localStorage.getItem('onboarding')) {
this.$router.push('/onboarding')
}
}
},
mounted() {
setTimeout(
function () {
this.activityNav = !this.$vuetify.breakpoint.smAndDown
}.bind(this),
10
{ fetchPolicy: 'cache-and-network' }
)
},
methods: {
onInviteUsed() {
// Refetch feed
this.$apollo.queries.timeline.refetch()
},
groupSimilarActivities(data) {
if (!data) return
const timeline = computed(() => {
return timelineResult.value?.user?.timeline || null
})
const groupedTimeline = computed(() => {
const data = timelineResult.value
if (!data) return []
const skippableActionTypes = ['stream_invite_sent', 'stream_invite_declined']
const groupedTimeline = data.user.timeline.items.reduce(function (prev, curr) {
@@ -206,31 +158,76 @@ export default {
}
return prev
}, [])
// console.log(groupedTimeline)
this.groupedTimeline = groupedTimeline
return groupedTimeline
})
// Quick user info
const { result: quickUserResult, loading: quickUserLoading } = useQuery(gql`
query {
quickUser: user {
id
name
}
}
`)
const quickUser = computed(() => quickUserResult.value?.quickUser || null)
return {
quickUser,
groupedTimeline,
timeline,
timelineFetchMore,
timelineRefetch,
quickUserLoading,
timelineLoading
}
},
data() {
return {
newStreamDialog: 0,
activityNav: true
}
},
computed: {
isApolloLoading() {
return this.$apollo.loading || this.quickUserLoading || this.timelineLoading
}
},
watch: {
timeline(val) {
if (val.totalCount === 0 && !localStorage.getItem('onboarding')) {
this.$router.push('/onboarding')
}
}
},
mounted() {
setTimeout(
function () {
this.activityNav = !this.$vuetify.breakpoint.smAndDown
}.bind(this),
10
)
},
methods: {
onInviteUsed() {
// Refetch feed
this.timelineRefetch()
},
infiniteHandler($state) {
this.$apollo.queries.timeline.fetchMore({
async infiniteHandler($state) {
const result = await this.timelineFetchMore({
variables: {
cursor: this.timeline.cursor
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
const newItems = fetchMoreResult.user.timeline.items
//set vue-infinite state
if (newItems.length === 0) $state.complete()
else $state.loaded()
fetchMoreResult.user.timeline.items = [
...previousResult.user.timeline.items,
...newItems
]
return fetchMoreResult
}
})
const newItems = result.data?.user?.timeline?.items || []
if (!newItems.length) {
$state.complete()
} else {
$state.loaded()
}
}
}
}
@@ -56,7 +56,7 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
components: {
@@ -23,7 +23,7 @@
<div slot="no-results">There are no activities to load</div>
</infinite-loading>
</v-timeline>
<v-timeline v-else-if="$apollo.loading" align-top dense>
<v-timeline v-else-if="isApolloLoading" align-top dense>
<v-timeline-item v-for="i in 6" :key="i" medium>
<v-skeleton-loader type="article"></v-skeleton-loader>
</v-timeline-item>
@@ -37,7 +37,10 @@
</v-row>
</template>
<script>
import gql from 'graphql-tag'
import { StreamWithActivityDocument } from '@/graphql/generated/graphql'
import { useQuery } from '@vue/apollo-composable'
import { useRoute } from '@/main/lib/core/composables/router'
import { computed } from 'vue'
export default {
name: 'StreamActivity',
@@ -45,56 +48,22 @@ export default {
ListItemActivity: () => import('@/main/components/activity/ListItemActivity'),
InfiniteLoading: () => import('vue-infinite-loading')
},
data() {
return { groupedActivity: null }
},
apollo: {
stream: {
query: gql`
query Stream($id: String!, $cursor: DateTime) {
stream(id: $id) {
id
name
createdAt
commits {
totalCount
}
branches {
totalCount
}
activity(cursor: $cursor) {
totalCount
cursor
items {
actionType
userId
streamId
resourceId
resourceType
time
info
message
}
}
}
}
`,
variables() {
return {
id: this.$route.params.streamId
}
},
result({ data }) {
this.groupSimilarActivities(data)
}
}
},
methods: {
groupSimilarActivities(data) {
if (!data) return
setup() {
// Stream activity query & derived computeds
const route = useRoute()
const {
result,
fetchMore: activityFetchMore,
loading: activityLoading
} = useQuery(StreamWithActivityDocument, () => ({
id: route.params.streamId,
cursor: null
}))
const stream = computed(() => result.value?.stream || null)
const skippableActionTypes = ['stream_invite_sent', 'stream_invite_declined']
const groupedActivity = data.stream.activity.items.reduce(function (prev, curr) {
const skippableActionTypes = ['stream_invite_sent', 'stream_invite_declined']
const groupedActivity = computed(() =>
(stream.value?.activity?.items || []).reduce(function (prev, curr) {
if (skippableActionTypes.includes(curr.actionType)) {
return prev
}
@@ -130,30 +99,35 @@ export default {
}
return prev
}, [])
// console.log(groupedTimeline)
this.groupedActivity = groupedActivity
},
infiniteHandler($state) {
this.$apollo.queries.stream.fetchMore({
)
return {
stream,
groupedActivity,
activityFetchMore,
activityLoading
}
},
computed: {
isApolloLoading() {
return this.$apollo.loading || this.activityLoading
}
},
methods: {
async infiniteHandler($state) {
const result = await this.activityFetchMore({
variables: {
id: this.$route.params.streamId,
cursor: this.stream.activity.cursor
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
const newItems = fetchMoreResult.stream.activity.items
//set vue-infinite state
if (newItems.length === 0) $state.complete()
else $state.loaded()
fetchMoreResult.stream.activity.items = [
...previousResult.stream.activity.items,
...newItems
]
return fetchMoreResult
}
})
const newItems = result.data?.stream?.activity?.items
if (!newItems.length) {
$state.complete()
} else {
$state.loaded()
}
}
}
}
@@ -12,7 +12,7 @@
<script lang="ts">
import Vue from 'vue'
import StreamInviteBanner from '@/main/components/stream/StreamInviteBanner.vue'
import { useUserStreamInvitesQuery } from '@/graphql/generated/graphql'
import { UserStreamInvitesDocument } from '@/graphql/generated/graphql'
import { StreamInviteType } from '@/main/lib/stream/mixins/streamInviteMixin'
export default Vue.extend({
@@ -24,7 +24,9 @@ export default Vue.extend({
streamInvites: [] as NonNullable<StreamInviteType[]>
}),
apollo: {
streamInvites: useUserStreamInvitesQuery()
streamInvites: {
query: UserStreamInvitesDocument
}
},
methods: {
onInviteUsed({ accept }: { accept: boolean }, invite: StreamInviteType) {
@@ -74,7 +74,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import webhookQuery from '@/graphql/webhook.gql'
export default {
@@ -45,7 +45,8 @@
<script lang="ts">
import Vue from 'vue'
import SectionCard from '@/main/components/common/SectionCard.vue'
import { leaveStreamMutation } from '@/graphql/generated/graphql'
import { LeaveStreamDocument } from '@/graphql/generated/graphql'
import { convertThrowIntoFetchResult } from '@/main/lib/common/apollo/helpers/apolloOperationHelper'
export default Vue.extend({
name: 'LeaveStreamPanel',
@@ -65,9 +66,14 @@ export default Vue.extend({
async leaveStream() {
const { streamId } = this
const { data, errors } = await leaveStreamMutation(this, {
variables: { streamId }
})
const results = await this.$apollo
.mutate({
mutation: LeaveStreamDocument,
variables: { streamId }
})
.catch(convertThrowIntoFetchResult)
const { data, errors } = results
if (data?.streamLeave) {
this.$triggerNotification({
@@ -13,7 +13,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { canBeFavorited } from '@/helpers/streamHelpers'
import { userFavoriteStreamsQuery } from '@/graphql/user'
import { commonStreamFieldsFragment } from '@/graphql/streams'
@@ -94,9 +94,10 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { randomString } from '@/helpers/randomHelpers'
import objectQuery from '@/graphql/objectSingle.gql'
import { omit } from 'lodash'
export default {
name: 'GlobalsBuilder',
@@ -114,10 +115,13 @@ export default {
}
},
update(data) {
delete data.stream.object.data.__closure
this.globalsArray = this.nestedGlobals(data.stream.object.data)
return data.stream.object
},
result({ data }) {
this.globalsArray = this.nestedGlobals(
omit(data.stream.object.data, ['__closure'])
)
},
skip() {
return !this.objectId
}
@@ -62,7 +62,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
props: {
@@ -87,7 +87,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { fullServerInfoQuery } from '@/graphql/server'
export default {
@@ -83,7 +83,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { fullServerInfoQuery } from '@/graphql/server'
export default {
@@ -38,7 +38,7 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
components: {},
@@ -67,7 +67,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import AppEditDialog from '@/main/components/user/AppEditDialog'
export default {
@@ -57,7 +57,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { fullServerInfoQuery } from '@/graphql/server'
export default {
@@ -32,7 +32,7 @@
</section-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
components: {
@@ -27,7 +27,7 @@
</section-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
components: {
SectionCard: () => import('@/main/components/common/SectionCard'),
@@ -68,7 +68,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
components: {},
@@ -27,7 +27,7 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import UserDeleteDialog from '@/main/dialogs/UserDeleteDialog'
import { signOut } from '@/plugins/authHelpers'
@@ -110,7 +110,7 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
components: {
VImageInput: () => import('vuetify-image-input/a-la-carte'),
@@ -231,7 +231,7 @@
</template>
<script>
import * as THREE from 'three'
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { debounce, throttle } from 'lodash'
import { getCamArray } from './viewerFrontendHelpers'
import CommentEditor from '@/main/components/comments/CommentEditor.vue'
@@ -170,7 +170,7 @@
<script>
import * as THREE from 'three'
import { debounce, throttle } from 'lodash'
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { VIEWER_UPDATE_THROTTLE_TIME } from '@/main/lib/viewer/comments/commentsHelper'
import { buildResizeHandlerMixin } from '@/main/lib/common/web-apis/mixins/windowResizeHandler'
import { documentToBasicString } from '@/main/lib/common/text-editor/documentHelper'
@@ -224,6 +224,10 @@ export default {
},
result({ data }) {
if (!data) return
// Only reason why it's OK to mutate apollo results here, is because
// of the 'no-cache' fetchPolicy, which means that none of the data here is actually
// mutating the Apollo Cache
for (const c of data.comments.items) {
c.expanded = false
c.hovered = false
@@ -262,7 +266,7 @@ export default {
skip() {
return !this.$loggedIn()
},
updateQuery(prevResult, { subscriptionData }) {
updateQuery(_, { subscriptionData }) {
if (!subscriptionData.data?.commentActivity) return
const { comment: newComment, type } = subscriptionData.data.commentActivity
@@ -277,7 +281,7 @@ export default {
newComment.archived = false
if (type === 'comment-added') {
if (prevResult.comments.items.find((c) => c.id === newComment.id)) {
if (this.localComments.find((c) => c.id === newComment.id)) {
return
}
if (!newComment.archived && newComment.data.location)
@@ -27,7 +27,7 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
name: 'ObjectProperties',
@@ -88,7 +88,7 @@
</template>
<script>
import * as THREE from 'three'
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { v4 as uuid } from 'uuid'
import debounce from 'lodash/debounce'
@@ -46,7 +46,10 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
export default {
components: {
InfiniteLoading: () => import('vue-infinite-loading'),
@@ -62,13 +65,13 @@ export default {
default: () => null
}
},
apollo: {
stream: {
query: gql`
setup(props) {
const { result: streamResult, fetchMore: streamFetchMore } = useQuery(
gql`
query ($streamId: String!, $cursor: String) {
stream(id: $streamId) {
id
commits(cursor: $cursor, limit: 2) {
commits(cursor: $cursor, limit: 6) {
totalCount
cursor
items {
@@ -84,51 +87,31 @@ export default {
}
}
`,
variables() {
return { streamId: this.streamId }
},
skip() {
return !this.streamId
}
() => ({ streamId: props.streamId }),
() => ({ enabled: !!props.streamId })
)
const stream = computed(() => streamResult.value?.stream)
return {
stream,
streamFetchMore
}
},
data() {
return {}
},
async mounted() {},
methods: {
infiniteHandler($state) {
this.$apollo.queries.stream.fetchMore({
async infiniteHandler($state) {
const result = await this.streamFetchMore({
variables: {
cursor: this.stream.commits.cursor,
streamId: this.streamId
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
const newItems = fetchMoreResult.stream.commits.items
if (newItems.length === 0) $state.complete()
else $state.loaded()
const allItems = [...previousResult.stream.commits.items]
for (const commit of newItems) {
if (allItems.findIndex((c) => c.id === commit.id) === -1)
allItems.push(commit)
}
return {
stream: {
__typename: previousResult.stream.__typename,
id: previousResult.stream.id,
commits: {
__typename: previousResult.stream.commits.__typename,
cursor: fetchMoreResult.stream.commits.cursor,
totalCount: fetchMoreResult.stream.commits.totalCount,
items: allItems
}
}
}
}
})
const newItems = result.data?.stream?.commits?.items || []
if (!newItems.length) {
$state.complete()
} else {
$state.loaded()
}
}
}
}
@@ -47,7 +47,9 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
export default {
components: {
InfiniteLoading: () => import('vue-infinite-loading'),
@@ -67,52 +69,67 @@ export default {
default: () => null
}
},
data() {
return {
skip: true,
cursor: new Date().toISOString(),
commits: []
}
},
async mounted() {
this.fetchBranchCommits()
},
methods: {
async fetchBranchCommits() {
const res = await this.$apollo.query({
query: gql`
query {
stream(id: "${this.streamId}") {
id
branch(name: "${this.branchName}") {
name
commits( cursor: "${this.cursor}", limit: 2) {
totalCount
cursor
items {
sourceApplication
id
createdAt
authorId
branchName
message
referencedObject
}
setup(props) {
const { result: commitsResult, fetchMore: commitsFetchMore } = useQuery(
gql`
query BranchAllCommits($sid: String!, $branchName: String, $cursor: String) {
stream(id: $sid) {
id
branch(name: $branchName) {
name
commits(cursor: $cursor, limit: 6) {
totalCount
cursor
items {
sourceApplication
id
createdAt
authorId
branchName
message
referencedObject
}
}
}
}
`
}
`,
() => ({
sid: props.streamId,
branchName: props.branchName,
cursor: null
})
const items = res.data.stream.branch.commits.items
this.cursor = res.data.stream.branch.commits.cursor
items.forEach((item) => this.commits.push(item))
return items
},
)
const commits = computed(
() => commitsResult.value?.stream?.branch?.commits?.items || []
)
const cursor = computed(
() => commitsResult?.value?.stream?.branch?.commits?.cursor || null
)
return {
commits,
cursor,
commitsFetchMore
}
},
methods: {
async infiniteHandler($state) {
const items = await this.fetchBranchCommits()
if (items.length === 0) $state.complete()
else $state.loaded()
const result = await this.commitsFetchMore({
variables: {
sid: this.streamId,
branchName: this.branchName,
cursor: this.cursor
}
})
const newItems = result?.data?.stream?.branch?.commits?.items || []
if (!newItems.length) {
$state.complete()
} else {
$state.loaded()
}
}
}
}
@@ -85,7 +85,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import streamObjectQuery from '@/graphql/objectSingleNoData.gql'
export default {
name: 'StreamOverlayViewer',
@@ -81,7 +81,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import isNull from 'lodash/isNull'
import isUndefined from 'lodash/isUndefined'
import clone from 'lodash/clone'
@@ -91,7 +91,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
props: {
stream: {
@@ -63,13 +63,13 @@
</div>
</template>
<script lang="ts">
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import Vue, { PropType } from 'vue'
import { email, maxLength, noXss, required } from '@/main/lib/common/vuetify/validators'
import { Nullable, Optional } from '@/helpers/typeHelpers'
import { VFormInstance } from '@/helpers/vuetifyHelpers'
import type { Get } from 'type-fest'
import type { FetchResult } from 'apollo-link'
import type { FetchResult } from '@apollo/client/core'
import { UserSearchQuery } from '@/graphql/generated/graphql'
import BasicUserInfoRow from '@/main/components/user/BasicUserInfoRow.vue'
import { StreamEvents } from '@/main/lib/core/helpers/eventHubHelper'
@@ -35,7 +35,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
export default {
data() {
@@ -50,18 +50,14 @@
placeholder="Search by name or by email"
/>
<div v-if="$apollo.loading">Searching.</div>
<v-list v-if="userSearch && userSearch.items" one-line>
<v-list-item v-if="userSearch.items.length === 0">
<v-list v-if="userSearch && users" one-line>
<v-list-item v-if="users.length === 0">
<v-list-item-content>
<v-list-item-title>No users found.</v-list-item-title>
<v-list-item-subtitle>Try a different search query.</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item
v-for="item in userSearch.items"
:key="item.id"
@click="addCollab(item)"
>
<v-list-item v-for="item in users" :key="item.id" @click="addCollab(item)">
<v-list-item-avatar>
<user-avatar
:id="item.id"
@@ -116,7 +112,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { userSearchQuery } from '@/graphql/user'
export default {
@@ -145,6 +141,9 @@ export default {
skip() {
return !this.search || this.search.length < 3
},
result({ data }) {
this.users = [...data.userSearch.items]
},
debounce: 300
}
},
@@ -157,14 +156,15 @@ export default {
nameRules: [],
isPublic: true,
collabs: [],
isLoading: false
isLoading: false,
users: null
}
},
watch: {
open() {
this.name = null
this.search = null
if (this.userSearch) this.userSearch.items = null
this.users = null
this.collabs = []
}
},
@@ -183,10 +183,10 @@ export default {
if (user.id === localStorage.getItem('uuid')) return
const indx = this.collabs.findIndex((u) => u.id === user.id)
if (indx !== -1) return
user.role = 'stream:contributor'
this.collabs.push(user)
this.search = null
this.userSearch.items = null
this.users = null
},
removeCollab(user) {
const indx = this.collabs.findIndex((u) => u.id === user.id)
@@ -218,7 +218,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { commonStreamFieldsFragment } from '@/graphql/streams'
import InviteDialog from '@/main/dialogs/InviteDialog.vue'
import UserAvatar from '@/main/components/common/UserAvatar.vue'
@@ -15,13 +15,12 @@
</v-col>
<v-col cols="11" sm="8" md="6" lg="4" xl="3">
<router-view></router-view>
<!-- Temporary revert of our no v-html policy: -->
<p
v-if="serverInfo"
class="caption text-center mt-2"
v-html="serverInfo.termsOfService"
>
<!-- Temporary revert of our no v-html policy -->
</p>
></p>
</v-col>
</v-row>
</v-container>
@@ -31,6 +30,8 @@
import LoginBlurb from '@/main/components/auth/LoginBlurb.vue'
import { mainServerInfoQuery } from '@/graphql/server'
// TODO: Need to fix the v-html usage ASAP
export default {
name: 'TheAuth',
components: { LoginBlurb },
@@ -61,9 +61,9 @@
</v-app>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { mainUserDataQuery } from '@/graphql/user'
import { mainServerInfoQuery } from '@/graphql/server'
import { setDarkTheme } from '@/main/utils/themeStateManager'
export default {
name: 'TheMain',
@@ -77,9 +77,6 @@ export default {
import('@/main/components/user/EmailVerificationBanner')
},
apollo: {
serverInfo: {
query: mainServerInfoQuery
},
user: {
query: mainUserDataQuery,
skip() {
@@ -148,10 +145,8 @@ export default {
methods: {
switchTheme() {
this.$vuetify.theme.dark = !this.$vuetify.theme.dark
localStorage.setItem(
'darkModeEnabled',
this.$vuetify.theme.dark ? 'dark' : 'light'
)
setDarkTheme(this.$vuetify.theme.dark, true)
this.$mixpanel.people.set(
'Theme Web',
this.$vuetify.theme.dark ? 'dark' : 'light'
@@ -0,0 +1,21 @@
import { ApolloError, FetchResult } from '@apollo/client/core'
import { GraphQLError } from 'graphql'
/**
* Convert an error thrown during $apollo.mutate() into a fetch result
*/
export function convertThrowIntoFetchResult(err: unknown): FetchResult {
let gqlErrors: readonly GraphQLError[]
if (err instanceof ApolloError) {
gqlErrors = err.graphQLErrors
} else if (err instanceof Error) {
gqlErrors = [new GraphQLError(err.message)]
} else {
gqlErrors = [new GraphQLError(err + '')]
}
return {
data: undefined,
errors: gqlErrors
}
}
@@ -0,0 +1,35 @@
import { Optional } from '@/helpers/typeHelpers'
import { ComposableInvokedOutOfScopeError } from '@/main/lib/core/errors/composition'
import { getCurrentInstance, reactive } from 'vue'
import VueRouter, { Route } from 'vue-router'
let currentRoute: Optional<Route>
/**
* Get router (not reactive)
*/
export function useRouter(): VueRouter {
const vm = getCurrentInstance()
if (!vm) throw new ComposableInvokedOutOfScopeError()
return vm.proxy.$router
}
/**
* Get current route object (reactive)
*/
export function useRoute(): Route {
if (currentRoute) return currentRoute
const router = useRouter()
const vm = getCurrentInstance()
if (!vm) throw new ComposableInvokedOutOfScopeError()
const newRoute = reactive({ ...vm.proxy.$route } as Route)
router.afterEach((to) => {
Object.assign(newRoute, to)
})
currentRoute = newRoute
return newRoute
}
@@ -0,0 +1,6 @@
import { BaseError } from '@/helpers/errorHelper'
export class ComposableInvokedOutOfScopeError extends BaseError {
static defaultMessage =
'getCurrentInstance() returned null. Method must be called at the top of a setup function'
}
@@ -0,0 +1,63 @@
import { Optional } from '@/helpers/typeHelpers'
import { FieldMergeFunction } from '@apollo/client/core'
interface AbstractCollection<T extends string> {
__typename: T
totalCount: number
cursor: string | null
items: Record<string, unknown>[]
}
/**
* Build an Apollo merge function for a field that returns a collection like AbstractCollection
* @param {{}} [param0]
* @param {boolean} [param0.checkIdentity] Set to true if you want to double check that items with IDs
* that already appear in old results, don't get added again
* @param {string} [param0.identityProp] Optionally change the prop that should be used to compare
* equality between items
*/
export function buildAbstractCollectionMergeFunction<T extends string>(
typeName: T,
{ checkIdentity = false, identityProp = '__ref' } = {}
): FieldMergeFunction<Optional<AbstractCollection<T>>, AbstractCollection<T>> {
return (
existing: Optional<AbstractCollection<T>>,
incoming: AbstractCollection<T>
) => {
const existingItems = existing?.items || []
const incomingItems = incoming?.items || []
let finalItems: Record<string, unknown>[]
if (checkIdentity) {
finalItems = [...existingItems]
for (const newItem of incomingItems) {
if (
finalItems.findIndex(
(item) => item[identityProp] === newItem[identityProp]
) === -1
) {
finalItems.push(newItem)
}
}
} else {
finalItems = [...existingItems, ...incomingItems]
}
return {
__typename: incoming?.__typename || existing?.__typename || typeName,
totalCount: incoming.totalCount || 0,
cursor: incoming.cursor || null,
items: finalItems
}
}
}
/**
* Merge function that just takes incoming data and overrides all of old data with it
* Useful for array fields w/o pagination, where a new array response is supposed to replace
* the entire old one
*/
export const incomingOverwritesExistingMergeFunction: FieldMergeFunction = (
_existing: unknown,
incoming: unknown
) => incoming
@@ -1,5 +1,5 @@
import Vue from 'vue'
import { useIsLoggedInQuery } from '@/graphql/generated/graphql'
import { IsLoggedInDocument, IsLoggedInQuery } from '@/graphql/generated/graphql'
/**
* Mixin for checking if user is logged in through Apollo Client. Use the reactive 'isLoggedIn' data property
* to check if a user is logged in.
@@ -13,8 +13,9 @@ export const IsLoggedInMixin = Vue.extend({
isLoggedIn: false
}),
apollo: {
isLoggedIn: useIsLoggedInQuery<Vue & { isLoggedIn: boolean }>({
update: (data) => !!data.user?.id
})
isLoggedIn: {
query: IsLoggedInDocument,
update: (data: IsLoggedInQuery) => !!data.user?.id
}
}
})
@@ -1,11 +1,12 @@
import {
StreamInviteQuery,
useStreamInviteMutation,
StreamInviteDocument,
UserStreamInvitesQuery,
UserStreamInvitesDocument
UserStreamInvitesDocument,
UseStreamInviteDocument
} from '@/graphql/generated/graphql'
import { MaybeFalsy, Nullable, vueWithMixins } from '@/helpers/typeHelpers'
import { convertThrowIntoFetchResult } from '@/main/lib/common/apollo/helpers/apolloOperationHelper'
import { StreamEvents } from '@/main/lib/core/helpers/eventHubHelper'
import { IsLoggedInMixin } from '@/main/lib/core/mixins/isLoggedInMixin'
import { Get } from 'type-fest'
@@ -68,76 +69,79 @@ export const UsersStreamInviteMixin = vueWithMixins(IsLoggedInMixin).extend({
async processInvite(accept: boolean) {
if (!this.token) return
const { data, errors } = await useStreamInviteMutation(this, {
variables: {
accept,
streamId: this.streamId,
token: this.token
},
update: (cache, { data }) => {
if (!data?.streamInviteUse) return
const { data, errors } = await this.$apollo
.mutate({
mutation: UseStreamInviteDocument,
variables: {
accept,
streamId: this.streamId,
token: this.token
},
update: (cache, { data }) => {
if (!data?.streamInviteUse) return
// It's weird that i'm emitting from inside the update handler, but if I invoke the emit
// at the bottom of `processInvite()`, the event won't be fired because of a race condition
// between the cache updates below and the queries that rely on the cached invites in the parent
// component. Basically - I have to do it this way or the event won't be handled
this.$emit('invite-used', { accept })
// It's weird that i'm emitting from inside the update handler, but if I invoke the emit
// at the bottom of `processInvite()`, the event won't be fired because of a race condition
// between the cache updates below and the queries that rely on the cached invites in the parent
// component. Basically - I have to do it this way or the event won't be handled
this.$emit('invite-used', { accept })
// Remove invite from various cached queries we might have
// 1. Single stream invite query
const singleStreamInviteCacheFilter = {
query: StreamInviteDocument,
variables: { streamId: this.streamId, token: this.token }
}
let singleStreamInviteQueryData: MaybeFalsy<StreamInviteQuery> = undefined
try {
singleStreamInviteQueryData = cache.readQuery<StreamInviteQuery>(
singleStreamInviteCacheFilter
)
} catch (err) {
// suppressed
}
if (singleStreamInviteQueryData?.streamInvite) {
cache.writeQuery({
...singleStreamInviteCacheFilter,
data: {
streamInvite: null
}
})
}
// 2. All user's stream invites query
let allUsersStreamInvitesQueryData: MaybeFalsy<UserStreamInvitesQuery> =
undefined
try {
allUsersStreamInvitesQueryData = cache.readQuery<UserStreamInvitesQuery>({
query: UserStreamInvitesDocument
})
} catch (err) {
// suppressed
}
if (allUsersStreamInvitesQueryData?.streamInvites) {
const removableInviteIdx =
allUsersStreamInvitesQueryData.streamInvites.findIndex(
(i) => i.inviteId === this.inviteId
// Remove invite from various cached queries we might have
// 1. Single stream invite query
const singleStreamInviteCacheFilter = {
query: StreamInviteDocument,
variables: { streamId: this.streamId, token: this.token }
}
let singleStreamInviteQueryData: MaybeFalsy<StreamInviteQuery> = undefined
try {
singleStreamInviteQueryData = cache.readQuery<StreamInviteQuery>(
singleStreamInviteCacheFilter
)
if (removableInviteIdx !== -1) {
const newInvites = allUsersStreamInvitesQueryData.streamInvites.slice()
newInvites.splice(removableInviteIdx, 1)
} catch (err) {
// suppressed
}
cache.writeQuery<UserStreamInvitesQuery>({
query: UserStreamInvitesDocument,
if (singleStreamInviteQueryData?.streamInvite) {
cache.writeQuery({
...singleStreamInviteCacheFilter,
data: {
...allUsersStreamInvitesQueryData,
streamInvites: newInvites
streamInvite: null
}
})
}
// 2. All user's stream invites query
let allUsersStreamInvitesQueryData: MaybeFalsy<UserStreamInvitesQuery> =
undefined
try {
allUsersStreamInvitesQueryData = cache.readQuery<UserStreamInvitesQuery>({
query: UserStreamInvitesDocument
})
} catch (err) {
// suppressed
}
if (allUsersStreamInvitesQueryData?.streamInvites) {
const removableInviteIdx =
allUsersStreamInvitesQueryData.streamInvites.findIndex(
(i) => i.inviteId === this.inviteId
)
if (removableInviteIdx !== -1) {
const newInvites = allUsersStreamInvitesQueryData.streamInvites.slice()
newInvites.splice(removableInviteIdx, 1)
cache.writeQuery<UserStreamInvitesQuery>({
query: UserStreamInvitesDocument,
data: {
...allUsersStreamInvitesQueryData,
streamInvites: newInvites
}
})
}
}
}
}
})
})
.catch(convertThrowIntoFetchResult)
if (data?.streamInviteUse) {
this.$triggerNotification({
@@ -132,6 +132,7 @@
<script>
import { mainUserDataQuery } from '@/graphql/user'
import InviteDialog from '@/main/dialogs/InviteDialog.vue'
import { setDarkTheme } from '@/main/utils/themeStateManager'
export default {
components: {
@@ -172,10 +173,8 @@ export default {
methods: {
switchTheme() {
this.$vuetify.theme.dark = !this.$vuetify.theme.dark
localStorage.setItem(
'darkModeEnabled',
this.$vuetify.theme.dark ? 'dark' : 'light'
)
setDarkTheme(this.$vuetify.theme.dark, true)
this.$mixpanel.people.set(
'Theme Web',
this.$vuetify.theme.dark ? 'dark' : 'light'
@@ -1,4 +1,3 @@
setDarkTheme
<template>
<div class="elevation-10">
<portal-target name="nav-bottom">
@@ -45,18 +44,9 @@ setDarkTheme
</template>
<script>
import { signOut } from '@/plugins/authHelpers'
import { mainUserDataQuery } from '@/graphql/user'
import { setDarkTheme } from '@/main/utils/themeStateManager'
export default {
apollo: {
user: {
query: mainUserDataQuery,
skip() {
return !this.loggedIn
}
}
},
methods: {
signOut() {
this.$mixpanel.track('Log Out', { type: 'action' })
@@ -221,7 +221,7 @@
</portal>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
+38 -51
View File
@@ -60,11 +60,13 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
} from '@/main/utils/portalStateManager'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
export default {
name: 'TheCommits',
@@ -74,66 +76,51 @@ export default {
NoDataPlaceholder: () => import('@/main/components/common/NoDataPlaceholder')
},
mixins: [buildPortalStateMixin([STANDARD_PORTAL_KEYS.Toolbar], 'commits', 0)],
apollo: {
user: {
query: gql`
query ($cursor: String) {
user {
id
name
commits(limit: 10, cursor: $cursor) {
totalCount
cursor
items {
id
referencedObject
message
streamName
streamId
createdAt
sourceApplication
branchName
commentCount
}
setup() {
const { result, fetchMore: userFetchMore } = useQuery(gql`
query ($cursor: String) {
user {
id
name
commits(limit: 10, cursor: $cursor) {
totalCount
cursor
items {
id
referencedObject
message
streamName
streamId
createdAt
sourceApplication
branchName
commentCount
}
}
}
`
}
`)
const user = computed(() => result.value?.user)
return {
user,
userFetchMore
}
},
methods: {
infiniteHandler($state) {
this.$apollo.queries.user.fetchMore({
async infiniteHandler($state) {
const result = await this.userFetchMore({
variables: {
cursor: this.user.commits.cursor
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
const newItems = fetchMoreResult.user.commits.items
if (newItems.length === 0) $state.complete()
else $state.loaded()
const allItems = [...previousResult.user.commits.items]
for (const commit of newItems) {
if (allItems.findIndex((c) => c.id === commit.id) === -1)
allItems.push(commit)
}
return {
user: {
__typename: previousResult.user.__typename,
name: previousResult.user.name,
id: previousResult.user.id,
commits: {
__typename: previousResult.user.commits.__typename,
cursor: fetchMoreResult.user.commits.cursor,
totalCount: fetchMoreResult.user.commits.totalCount,
items: allItems
}
}
}
}
})
const newItems = result.data?.user?.commits?.items || []
if (!newItems.length) {
$state.complete()
} else {
$state.loaded()
}
}
}
}
@@ -36,16 +36,16 @@ usePortalState
</div>
</template>
<script lang="ts">
import Vue, { defineComponent } from 'vue'
import { computed, defineComponent } from 'vue'
import { STANDARD_PORTAL_KEYS, usePortalState } from '@/main/utils/portalStateManager'
import {
UserFavoriteStreamsQuery,
UserFavoriteStreamsQueryVariables,
useUserFavoriteStreamsQuery
UserFavoriteStreamsDocument
} from '@/graphql/generated/graphql'
import type { StateChanger } from 'vue-infinite-loading'
import type { Get } from 'type-fest'
import type { SmartQuery } from 'vue-apollo/types/vue-apollo'
import { useQuery } from '@vue/apollo-composable'
import { Nullable } from '@/helpers/typeHelpers'
export default defineComponent({
name: 'TheFavoriteStreams',
@@ -61,15 +61,13 @@ export default defineComponent({
'favorite-streams',
0
)
return { canRenderToolbarPortal }
},
data() {
return {
user: undefined as UserFavoriteStreamsQuery['user']
}
},
apollo: {
user: useUserFavoriteStreamsQuery()
const { result, fetchMore: userFetchMore } = useQuery(UserFavoriteStreamsDocument, {
cursor: null as Nullable<string>
})
const user = computed(() => result.value?.user)
return { canRenderToolbarPortal, user, userFetchMore }
},
computed: {
streams(): NonNullable<
@@ -88,7 +86,7 @@ export default defineComponent({
}
},
methods: {
infiniteHandler($state: StateChanger) {
async infiniteHandler($state: StateChanger) {
if (this.allStreamsLoaded) {
$state.loaded()
$state.complete()
@@ -96,44 +94,18 @@ export default defineComponent({
}
// Fetch more favorites
const userQuery: SmartQuery<
Vue,
UserFavoriteStreamsQuery,
UserFavoriteStreamsQueryVariables
> = this.$apollo.queries.user
userQuery.fetchMore({
const result = await this.userFetchMore({
variables: {
cursor: this.user?.favoriteStreams?.cursor || null
},
updateQuery: (previousResult, { fetchMoreResult }) => {
const newFavorites = fetchMoreResult?.user?.favoriteStreams
const oldFavorites = previousResult.user?.favoriteStreams
let { items: newItems } = newFavorites || {}
let { items: allItems } = oldFavorites || {}
newItems ||= []
allItems ||= []
for (const stream of newItems) {
if (allItems.findIndex((s) => s.id === stream.id) === -1)
allItems.push(stream)
}
// set vue-infinite state
newItems.length === 0 ? $state.complete() : $state.loaded()
return {
...previousResult,
user: {
...previousResult.user,
favoriteStreams: {
...(fetchMoreResult?.user?.favoriteStreams || {}),
items: allItems
}
}
} as UserFavoriteStreamsQuery
}
})
const newItems = result?.data?.user?.favoriteStreams?.items || []
if (!newItems.length) {
$state.complete()
} else {
$state.loaded()
}
}
}
})
+29 -36
View File
@@ -92,6 +92,8 @@ import UserStreamInviteBanners from '@/main/components/stream/UserStreamInviteBa
import InfiniteLoading from 'vue-infinite-loading'
import StreamPreviewCard from '@/main/components/common/StreamPreviewCard.vue'
import NoDataPlaceholder from '@/main/components/common/NoDataPlaceholder.vue'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
export default {
name: 'TheStreams',
@@ -103,13 +105,26 @@ export default {
},
mixins: [buildPortalStateMixin([STANDARD_PORTAL_KEYS.Toolbar], 'streams', 0)],
apollo: {
streams: {
query: streamsQuery
},
user: {
query: mainUserDataQuery
}
},
setup() {
const {
result,
fetchMore: streamsFetchMore,
refetch: streamsRefetch
} = useQuery(streamsQuery, {
cursor: null
})
const streams = computed(() => result.value?.streams)
return {
streams,
streamsFetchMore,
streamsRefetch
}
},
data() {
return {
streamFilter: 1,
@@ -142,14 +157,14 @@ export default {
},
mounted() {
if (this.$route.query.refresh) {
this.$apollo.queries.streams.refetch()
this.streamsRefetch()
this.$router.replace({ path: this.$route.path, query: null })
}
},
methods: {
onInviteUsed() {
// Refetch streams
this.$apollo.queries.streams.refetch()
this.streamsRefetch()
},
checkFilter(role) {
if (this.streamFilter === 1) return true
@@ -158,47 +173,25 @@ export default {
if (this.streamFilter === 4 && role === 'stream:reviewer') return true
return false
},
// TODO: Prevent extra load if we've hit all items
infiniteHandler($state) {
async infiniteHandler($state) {
if (this.allStreamsLoaded) {
$state.loaded()
$state.complete()
return
}
this.$apollo.queries.streams.fetchMore({
const result = await this.streamsFetchMore({
variables: {
cursor: this.streams?.cursor
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
const newItems = fetchMoreResult.streams.items
const allItems = [...previousResult.streams.items]
const newTotalCount = fetchMoreResult.streams.totalCount
for (const stream of newItems) {
if (allItems.findIndex((s) => s.id === stream.id) === -1)
allItems.push(stream)
}
// Update infinite loader state
if (newItems.length === 0) {
$state.complete()
} else {
$state.loaded()
}
return {
streams: {
__typename: previousResult.streams.__typename,
totalCount: newTotalCount,
cursor: fetchMoreResult.streams.cursor,
// Merging the new streams
items: allItems
}
}
}
})
const newItems = result?.data?.streams?.items || []
if (!newItems.length) {
$state.complete()
} else {
$state.loaded()
}
}
}
}
@@ -20,7 +20,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
@@ -89,7 +89,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { isEmailValid } from '@/plugins/authHelpers'
import { mainServerInfoQuery } from '@/graphql/server'
import {
@@ -98,9 +98,10 @@ import {
} from '@/main/utils/portalStateManager'
import { maxLength, noXss } from '@/main/lib/common/vuetify/validators'
import {
batchInviteToServerMutation,
batchInviteToStreamsMutation
BatchInviteToStreamsDocument,
BatchInviteToServerDocument
} from '@/graphql/generated/graphql'
import { convertThrowIntoFetchResult } from '@/main/lib/common/apollo/helpers/apolloOperationHelper'
export default {
name: 'AdminInvites',
@@ -220,12 +221,18 @@ export default {
},
async sendInvites(paramsArray, streamId) {
const { data, errors } = streamId
? await batchInviteToStreamsMutation(this, {
variables: { paramsArray }
})
: await batchInviteToServerMutation(this, {
variables: { paramsArray }
})
? await this.$apollo
.mutate({
mutation: BatchInviteToStreamsDocument,
variables: { paramsArray }
})
.catch(convertThrowIntoFetchResult)
: await this.$apollo
.mutate({
mutation: BatchInviteToServerDocument,
variables: { paramsArray }
})
.catch(convertThrowIntoFetchResult)
if (!data?.serverInviteBatchCreate && !data?.streamInviteBatchCreate) {
const errMsg =
@@ -44,7 +44,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { mainServerInfoQuery } from '@/graphql/server'
import pick from 'lodash/pick'
import {
@@ -53,7 +53,7 @@ import {
} from '@/main/utils/portalStateManager'
export default {
name: 'ServerInfoAdminCard',
name: 'ServerSettings',
components: {
SectionCard: () => import('@/main/components/common/SectionCard')
},
@@ -95,10 +95,11 @@ export default {
apollo: {
serverInfo: {
query: mainServerInfoQuery,
update(data) {
delete data.serverInfo.__typename
this.serverModifications = Object.assign({}, data.serverInfo)
return data.serverInfo
result({ data }) {
const newModifications = Object.assign({}, data.serverInfo)
delete newModifications.__typename
this.serverModifications = newModifications
}
}
},
@@ -106,7 +106,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import debounce from 'lodash/debounce'
import {
STANDARD_PORTAL_KEYS,
@@ -102,20 +102,21 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import debounce from 'lodash/debounce'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
} from '@/main/utils/portalStateManager'
import {
deleteInviteMutation,
resendInviteMutation,
useAdminUsersListQuery
DeleteInviteDocument,
ResendInviteDocument,
AdminUsersListDocument
} from '@/graphql/generated/graphql'
import SectionCard from '@/main/components/common/SectionCard.vue'
import UsersListItem from '@/main/components/admin/UsersListItem.vue'
import { Roles } from '@/helpers/mainConstants'
import { convertThrowIntoFetchResult } from '@/main/lib/common/apollo/helpers/apolloOperationHelper'
// TODO: This needs a redesign, it's pretty unusable on small screens
@@ -190,9 +191,12 @@ export default {
},
methods: {
async deleteInvite({ inviteId }) {
const { data, errors } = await deleteInviteMutation(this, {
variables: { inviteId }
})
const { data, errors } = await this.$apollo
.mutate({
mutation: DeleteInviteDocument,
variables: { inviteId }
})
.catch(convertThrowIntoFetchResult)
if (data?.inviteDelete) {
this.refetch()
@@ -210,9 +214,12 @@ export default {
}
},
async resendInvite({ inviteId }) {
const { data, errors } = await resendInviteMutation(this, {
variables: { inviteId }
})
const { data, errors } = await this.$apollo
.mutate({
mutation: ResendInviteDocument,
variables: { inviteId }
})
.catch(convertThrowIntoFetchResult)
if (data?.inviteResend) {
this.$triggerNotification({
@@ -311,7 +318,8 @@ export default {
}
},
apollo: {
adminUsers: useAdminUsersListQuery({
adminUsers: {
query: AdminUsersListDocument,
variables() {
return {
limit: this.queryLimit,
@@ -319,7 +327,7 @@ export default {
query: this.q
}
}
})
}
}
}
</script>
@@ -77,7 +77,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import UserAvatar from '@/main/components/auth/UserAvatarAuthoriseApp'
import UserAvatarIcon from '@/main/components/common/UserAvatarIcon'
@@ -87,7 +87,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import debounce from 'lodash/debounce'
export default {
@@ -41,7 +41,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { isEmailValid } from '@/plugins/authHelpers'
export default {
@@ -102,7 +102,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import AuthStrategies from '@/main/components/auth/AuthStrategies.vue'
import { randomString } from '@/helpers/randomHelpers'
import { isEmailValid } from '@/plugins/authHelpers'
@@ -181,7 +181,7 @@
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import debounce from 'lodash/debounce'
import { randomString } from '@/helpers/randomHelpers'
@@ -1,154 +0,0 @@
<template>
<v-row>
<v-col cols="12">
<v-timeline
v-if="stream && groupedActivity && groupedActivity.length !== 0"
align-top
dense
>
<list-item-activity
v-for="activity in groupedActivity"
:key="activity.time"
:activity="activity"
:activity-group="activity"
class="my-1"
></list-item-activity>
<infinite-loading
v-if="
stream.activity && stream.activity.items.length < stream.activity.totalCount
"
@infinite="infiniteHandler"
>
<div slot="no-more">This is all your activity!</div>
<div slot="no-results">There are no ctivities to load</div>
</infinite-loading>
</v-timeline>
<v-timeline v-else-if="$apollo.loading" align-top dense>
<v-timeline-item v-for="i in 6" :key="i" medium>
<v-skeleton-loader type="article"></v-skeleton-loader>
</v-timeline-item>
</v-timeline>
<div v-if="groupedActivity && groupedActivity.length === 0">
<v-card class="transparent elevation-0 mt-10">
<v-card-text>Nothing to show 🍃</v-card-text>
</v-card>
</div>
</v-col>
</v-row>
</template>
<script>
import gql from 'graphql-tag'
// TODO: Is this unused?
export default {
name: 'TheActivity',
components: {
ListItemActivity: () => import('@/main/components/activity/ListItemActivity'),
InfiniteLoading: () => import('vue-infinite-loading')
},
data() {
return { groupedActivity: null }
},
apollo: {
stream: {
query: gql`
query Stream($id: String!, $cursor: DateTime) {
stream(id: $id) {
id
name
createdAt
commits {
totalCount
}
branches {
totalCount
}
activity(cursor: $cursor) {
totalCount
cursor
items {
actionType
userId
streamId
resourceId
resourceType
time
info
message
}
}
}
}
`,
variables() {
return {
id: this.$route.params.streamId
}
},
result({ data }) {
this.groupSimilarActivities(data)
}
}
},
methods: {
groupSimilarActivities(data) {
const groupedActivity = data.stream.activity.items.reduce(function (prev, curr) {
//first item
if (!prev.length) {
prev.push([curr])
return prev
}
const test = prev[prev.length - 1][0]
let action = 'split' // split | combine | skip
if (curr.actionType === test.actionType && curr.streamId === test.streamId) {
if (curr.actionType.includes('stream_permissions')) {
//skip multiple stream_permission actions on the same user, just pick the last!
if (
prev[prev.length - 1].some(
(x) => x.info.targetUser === curr.info.targetUser
)
)
action = 'skip'
else action = 'combine'
} //stream, branch, commit
else if (
curr.actionType.includes('_update') ||
curr.actionType === 'commit_create'
)
action = 'combine'
}
if (action === 'combine') {
prev[prev.length - 1].push(curr)
} else if (action === 'split') {
prev.push([curr])
}
return prev
}, [])
this.groupedActivity = groupedActivity
},
infiniteHandler($state) {
this.$apollo.queries.stream.fetchMore({
variables: {
cursor: this.stream.activity.cursor
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
const newItems = fetchMoreResult.stream.activity.items
//set vue-infinite state
if (newItems.length === 0) $state.complete()
else $state.loaded()
fetchMoreResult.stream.activity.items = [
...previousResult.stream.activity.items,
...newItems
]
return fetchMoreResult
}
})
}
}
}
</script>
@@ -1,5 +1,5 @@
<template>
<div class="">
<div>
<branch-toolbar
v-if="canRenderToolbarPortal && stream && stream.branch"
:stream="stream"
@@ -83,14 +83,14 @@
<no-data-placeholder
v-if="
!$apollo.loading && stream.branch && stream.branch.commits.totalCount === 0
!isApolloLoading && stream.branch && stream.branch.commits.totalCount === 0
"
>
<h2 class="space-grotesk">Branch "{{ stream.branch.name }}" has no commits.</h2>
</no-data-placeholder>
</v-row>
<error-placeholder
v-if="!$apollo.loading && (error || stream.branch === null)"
v-if="!isApolloLoading && (error || stream.branch === null)"
error-type="404"
>
<h2>{{ error || `Branch "${$route.params.branchName}" does not exist.` }}</h2>
@@ -98,12 +98,15 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import branchQuery from '@/graphql/branch.gql'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
} from '@/main/utils/portalStateManager'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import { useRoute } from '@/main/lib/core/composables/router'
export default {
name: 'TheBranch',
@@ -117,6 +120,31 @@ export default {
CommitPreviewCard: () => import('@/main/components/common/CommitPreviewCard')
},
mixins: [buildPortalStateMixin([STANDARD_PORTAL_KEYS.Toolbar], 'stream-branch', 1)],
setup() {
const route = useRoute()
const {
result,
fetchMore: streamFetchMore,
refetch: streamRefetch,
loading: streamLoading
} = useQuery(
branchQuery,
() => ({
streamId: route.params.streamId,
branchName: (route.params.branchName || '').toLowerCase(),
cursor: null
}),
{ fetchPolicy: 'network-only' }
)
const stream = computed(() => result.value?.stream)
return {
stream,
streamFetchMore,
streamRefetch,
streamLoading
}
},
data() {
return {
branchEditDialog: false,
@@ -125,16 +153,6 @@ export default {
}
},
apollo: {
stream: {
query: branchQuery,
variables() {
return {
streamId: this.streamId,
branchName: this.$route.params.branchName.toLowerCase()
}
},
fetchPolicy: 'network-only'
},
$subscribe: {
commitCreated: {
query: gql`
@@ -148,7 +166,7 @@ export default {
}
},
result() {
this.$apollo.queries.stream.refetch()
this.streamRefetch()
},
error(err) {
this.$eventHub.$emit('notification', {
@@ -171,7 +189,7 @@ export default {
}
},
result() {
this.$apollo.queries.stream.refetch()
this.streamRefetch()
},
error(err) {
this.$eventHub.$emit('notification', {
@@ -185,6 +203,9 @@ export default {
}
},
computed: {
isApolloLoading() {
return this.$apollo.loading || this.streamLoading
},
loggedInUserId() {
return localStorage.getItem('uuid')
},
@@ -192,36 +213,22 @@ export default {
return this.$route.params.streamId
},
latestCommitObjectUrl() {
if (
this.stream &&
this.stream.branch &&
this.stream.branch.commits.items &&
this.stream.branch.commits.items.length > 0
)
if ((this.stream?.branch?.commits?.items || []).length > 0)
return `${window.location.origin}/streams/${this.stream.id}/objects/${this.stream.branch.commits.items[0].referencedObject}`
else return null
},
latestCommit() {
if (
this.stream.branch.commits.items &&
this.stream.branch.commits.items.length > 0
)
if ((this.stream?.branch?.commits?.items || []).length > 0)
return this.stream.branch.commits.items[0]
else return null
},
allPreviousCommits() {
if (
this.stream.branch.commits.items &&
this.stream.branch.commits.items.length > 0
)
if ((this.stream?.branch?.commits?.items || []).length > 0)
return this.stream.branch.commits.items.slice(1)
else return null
},
allCommits() {
if (
this.stream.branch.commits.items &&
this.stream.branch.commits.items.length > 0
)
if ((this.stream?.branch?.commits?.items || []).length > 0)
return this.stream.branch.commits.items
else return []
}
@@ -231,44 +238,21 @@ export default {
this.$router.push(`/streams/${this.$route.params.streamId}/globals`)
},
methods: {
infiniteHandler($state) {
this.$apollo.queries.stream.fetchMore({
async infiniteHandler($state) {
const result = await this.streamFetchMore({
variables: {
cursor: this.stream.branch.commits.cursor
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
const newItems = fetchMoreResult.stream.branch.commits.items
if (newItems.length === 0) $state.complete()
else $state.loaded()
const allItems = [...previousResult.stream.branch.commits.items]
for (const commit of newItems) {
if (allItems.findIndex((c) => c.id === commit.id) === -1)
allItems.push(commit)
}
return {
stream: {
__typename: previousResult.stream.__typename,
name: previousResult.stream.name,
id: previousResult.stream.id,
branch: {
id: fetchMoreResult.stream.branch.id,
name: fetchMoreResult.stream.branch.name,
description: fetchMoreResult.stream.branch.description,
__typename: previousResult.stream.branch.__typename,
commits: {
__typename: previousResult.stream.branch.commits.__typename,
cursor: fetchMoreResult.stream.branch.commits.cursor,
totalCount: fetchMoreResult.stream.branch.commits.totalCount,
items: allItems
}
}
}
}
streamId: this.streamId,
branchName: this.$route.params.branchName.toLowerCase(),
cursor: this.stream?.branch?.commits?.cursor
}
})
const newItems = result?.data?.stream?.branch?.commits?.items || []
if (!newItems.length) {
$state.complete()
} else {
$state.loaded()
}
}
}
}
@@ -123,7 +123,7 @@
</v-container>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { fullServerInfoQuery } from '@/graphql/server'
import {
STANDARD_PORTAL_KEYS,
@@ -135,16 +135,16 @@ import InviteDialog from '@/main/dialogs/InviteDialog.vue'
import { userSearchQuery } from '@/graphql/user'
import StreamRoleCollaborators from '@/main/components/stream/collaborators/StreamRoleCollaborators.vue'
import {
cancelStreamInviteMutation,
useStreamWithCollaboratorsQuery,
StreamWithCollaboratorsDocument,
updateStreamPermissionMutation
CancelStreamInviteDocument,
UpdateStreamPermissionDocument
} from '@/graphql/generated/graphql'
import { StreamEvents } from '@/main/lib/core/helpers/eventHubHelper'
import { Roles } from '@/helpers/mainConstants'
import LeaveStreamPanel from '@/main/components/stream/collaborators/LeaveStreamPanel.vue'
import { IsLoggedInMixin } from '@/main/lib/core/mixins/isLoggedInMixin'
import { vueWithMixins } from '@/helpers/typeHelpers'
import { convertThrowIntoFetchResult } from '@/main/lib/common/apollo/helpers/apolloOperationHelper'
export default vueWithMixins(IsLoggedInMixin).extend({
// @vue/component
@@ -171,7 +171,8 @@ export default vueWithMixins(IsLoggedInMixin).extend({
inviteDialogUser: null
}),
apollo: {
stream: useStreamWithCollaboratorsQuery({
stream: {
query: StreamWithCollaboratorsDocument,
// Custom error policy so that a failing pendingCollaborators resolver (due to access rights)
// doesn't kill the entire query
errorPolicy: 'all',
@@ -181,7 +182,7 @@ export default vueWithMixins(IsLoggedInMixin).extend({
id: this.streamId
}
}
}),
},
userSearch: {
query: userSearchQuery,
variables() {
@@ -277,40 +278,43 @@ export default vueWithMixins(IsLoggedInMixin).extend({
const { streamId } = this
this.loading = true
const { data, errors } = await cancelStreamInviteMutation(this, {
variables: {
streamId,
inviteId
},
update(store, result) {
if (!result.data?.streamInviteCancel) return
const { data, errors } = await this.$apollo
.mutate({
mutation: CancelStreamInviteDocument,
variables: {
streamId,
inviteId
},
update(store, result) {
if (!result.data?.streamInviteCancel) return
// Read current stream info
const cachedData = store.readQuery({
query: StreamWithCollaboratorsDocument,
variables: { id: streamId }
})
const pendingCollaborators = cachedData?.stream?.pendingCollaborators
if (!pendingCollaborators) return
// Read current stream info
const cachedData = store.readQuery({
query: StreamWithCollaboratorsDocument,
variables: { id: streamId }
})
const pendingCollaborators = cachedData?.stream?.pendingCollaborators
if (!pendingCollaborators) return
// Remove collaborator
const newPendingCollaborators = pendingCollaborators.filter(
(c) => c.inviteId !== inviteId
)
const newData = {
stream: {
...cachedData.stream,
pendingCollaborators: newPendingCollaborators
// Remove collaborator
const newPendingCollaborators = pendingCollaborators.filter(
(c) => c.inviteId !== inviteId
)
const newData = {
stream: {
...cachedData.stream,
pendingCollaborators: newPendingCollaborators
}
}
}
store.writeQuery({
query: StreamWithCollaboratorsDocument,
variables: { id: streamId },
data: newData
})
}
})
store.writeQuery({
query: StreamWithCollaboratorsDocument,
variables: { id: streamId },
data: newData
})
}
})
.catch(convertThrowIntoFetchResult)
if (!data?.streamInviteCancel) {
const gqlError = errors?.[0]
@@ -378,15 +382,18 @@ export default vueWithMixins(IsLoggedInMixin).extend({
},
async updateUserPermission(userId, role) {
this.$mixpanel.track('Permission Action', { type: 'action', name: 'update' })
const { data, errors } = await updateStreamPermissionMutation(this, {
variables: {
params: {
streamId: this.stream.id,
userId,
role
const { data, errors } = await this.$apollo
.mutate({
mutation: UpdateStreamPermissionDocument,
variables: {
params: {
streamId: this.stream.id,
userId,
role
}
}
}
})
})
.catch(convertThrowIntoFetchResult)
if (!data?.streamUpdatePermission) {
const errMsg = errors?.[0]?.message || 'An unexpected issue occurred'
@@ -90,7 +90,7 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
@@ -171,7 +171,7 @@ export default {
variables() {
return { streamId: this.$route.params.streamId }
},
updateQuery(prevResult, { subscriptionData }) {
updateQuery(_, { subscriptionData }) {
const { comment } = subscriptionData.data.commentActivity
if (this.localComments.findIndex((lc) => comment.id === lc.id) === -1) {
this.localComments.push(comment)
@@ -147,7 +147,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import branchQuery from '@/graphql/branch.gql'
import {
STANDARD_PORTAL_KEYS,
@@ -174,7 +174,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
@@ -37,27 +37,27 @@
</template>
<script lang="ts">
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import StreamInviteBanner from '@/main/components/stream/StreamInviteBanner.vue'
import { StreamEvents } from '@/main/lib/core/helpers/eventHubHelper'
import Vue from 'vue'
import { Nullable, MaybeFalsy } from '@/helpers/typeHelpers'
import { ApolloError } from 'vue-apollo-smart-ops'
import {
useStreamInviteQuery,
useMainUserDataQuery,
StreamInviteDocument,
MainUserDataDocument,
MainUserDataQuery,
StreamQuery,
StreamDocument,
StreamQueryVariables
StreamQueryVariables,
StreamDocument
} from '@/graphql/generated/graphql'
import type { ApolloQueryResult } from 'apollo-client'
import type { ApolloQueryResult, ApolloError } from '@apollo/client/core'
import type { Get } from 'type-fest'
import StreamInvitePlaceholder from '@/main/components/stream/StreamInvitePlaceholder.vue'
import { StreamInviteType } from '@/main/lib/stream/mixins/streamInviteMixin'
import { getInviteTokenFromRoute } from '@/main/lib/auth/services/authService'
// Cause of a limitation of vue-apollo-smart-ops, this needs to be duplicated
// Cause of a limitation of Vue Apollo Options API TS types, this needs to be duplicated
// (the better option is to just use the Composition API)
type VueThis = Vue & {
streamId: string
inviteToken: Nullable<string>
@@ -118,14 +118,15 @@ export default Vue.extend({
}
},
apollo: {
streamInvite: useStreamInviteQuery<VueThis>({
variables() {
streamInvite: {
query: StreamInviteDocument,
variables(this: VueThis) {
return {
streamId: this.streamId,
token: this.inviteToken
}
}
}),
},
stream: {
query: StreamDocument,
variables(this: VueThis): StreamQueryVariables {
@@ -142,7 +143,9 @@ export default Vue.extend({
}
}
},
user: useMainUserDataQuery(),
user: {
query: MainUserDataDocument
},
$subscribe: {
branchCreated: {
query: gql`
@@ -130,7 +130,7 @@
</div>
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import { COMMENT_FULL_INFO_FRAGMENT } from '@/graphql/comments'
export default {
@@ -127,7 +127,7 @@
</template>
<script>
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
@@ -108,8 +108,8 @@
style="cursor: default"
>
<v-list-item-icon>
<v-icon :color="wh.statusIcon.color" class="pt-2">
{{ wh.statusIcon.icon }}
<v-icon :color="getStatusIcon(wh).color" class="pt-2">
{{ getStatusIcon(wh).icon }}
</v-icon>
</v-list-item-icon>
<v-list-item-content>
@@ -291,12 +291,6 @@ export default {
streamId: this.$route.params.streamId
}
},
update(data) {
data.stream.webhooks.items.forEach((wh) => {
wh.statusIcon = this.getStatusIcon(wh)
})
return data.stream
},
error(err) {
if (err.message) this.error = err.message.replace('GraphQL error: ', '')
else this.error = err
@@ -41,7 +41,7 @@
</template>
<script>
import ListItemStream from '@/main/components/user/ListItemStream'
import gql from 'graphql-tag'
import { gql } from '@apollo/client/core'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin

Some files were not shown because too many files have changed in this diff Show More