chore(fe1): remove deprecated frontend (#3998)
--------- Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
@@ -93,7 +93,6 @@ workflows:
|
||||
- get-version
|
||||
- deployment-testing-approval
|
||||
- docker-build-server
|
||||
- docker-build-frontend
|
||||
- docker-build-frontend-2
|
||||
- docker-build-previews
|
||||
- docker-build-webhooks
|
||||
@@ -116,12 +115,6 @@ workflows:
|
||||
requires:
|
||||
- get-version
|
||||
|
||||
- docker-build-frontend:
|
||||
context: *build-context
|
||||
filters: *filters-build
|
||||
requires:
|
||||
- get-version
|
||||
|
||||
- docker-build-frontend-2:
|
||||
context: *build-context
|
||||
filters: *filters-build
|
||||
@@ -195,22 +188,6 @@ workflows:
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-frontend:
|
||||
context: *docker-hub-context
|
||||
filters: *filters-publish
|
||||
requires:
|
||||
- docker-build-frontend
|
||||
- get-version
|
||||
- pre-commit
|
||||
- publish-approval
|
||||
- test-frontend-2
|
||||
- test-viewer
|
||||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-frontend-2:
|
||||
context: *docker-hub-context
|
||||
filters: *filters-publish
|
||||
@@ -340,7 +317,6 @@ workflows:
|
||||
- deployment-test-helm-chart
|
||||
- docker-publish-docker-compose-ingress
|
||||
- docker-publish-file-imports
|
||||
- docker-publish-frontend
|
||||
- docker-publish-frontend-2
|
||||
- docker-publish-monitor-container
|
||||
- docker-publish-previews
|
||||
@@ -494,6 +470,7 @@ jobs:
|
||||
S3_CREATE_BUCKET: 'true'
|
||||
REDIS_URL: 'redis://127.0.0.1:6379'
|
||||
S3_REGION: '' # optional, defaults to 'us-east-1'
|
||||
FRONTEND_ORIGIN: 'http://127.0.0.1:8081'
|
||||
ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
|
||||
ENABLE_ALL_FFS: 'true'
|
||||
RATELIMITER_ENABLED: 'false'
|
||||
@@ -566,6 +543,7 @@ jobs:
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: 'postgres://speckle:speckle@127.0.0.1:5432/speckle2_test'
|
||||
PGDATABASE: speckle2_test
|
||||
POSTGRES_MAX_CONNECTIONS_SERVER: 20
|
||||
PGUSER: speckle
|
||||
SESSION_SECRET: 'keyboard cat'
|
||||
STRATEGY_LOCAL: 'true'
|
||||
@@ -577,6 +555,7 @@ jobs:
|
||||
S3_CREATE_BUCKET: 'true'
|
||||
REDIS_URL: 'redis://127.0.0.1:6379'
|
||||
S3_REGION: '' # optional, defaults to 'us-east-1'
|
||||
FRONTEND_ORIGIN: 'http://127.0.0.1:8081'
|
||||
ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
|
||||
DISABLE_ALL_FFS: 'true'
|
||||
RATELIMITER_ENABLED: 'false'
|
||||
@@ -627,6 +606,7 @@ jobs:
|
||||
S3_CREATE_BUCKET: 'true'
|
||||
REDIS_URL: 'redis://127.0.0.1:6379'
|
||||
S3_REGION: '' # optional, defaults to 'us-east-1'
|
||||
FRONTEND_ORIGIN: 'http://127.0.0.1:8081'
|
||||
ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
|
||||
FF_BILLING_INTEGRATION_ENABLED: 'true'
|
||||
# These are the only different env keys:
|
||||
@@ -1018,11 +998,6 @@ jobs:
|
||||
environment:
|
||||
SPECKLE_SERVER_PACKAGE: server
|
||||
|
||||
docker-build-frontend:
|
||||
<<: *build-job
|
||||
environment:
|
||||
SPECKLE_SERVER_PACKAGE: frontend
|
||||
|
||||
docker-build-frontend-2:
|
||||
<<: *build-job
|
||||
resource_class: xlarge
|
||||
@@ -1088,11 +1063,6 @@ jobs:
|
||||
environment:
|
||||
SPECKLE_SERVER_PACKAGE: server
|
||||
|
||||
docker-publish-frontend:
|
||||
<<: *publish-job
|
||||
environment:
|
||||
SPECKLE_SERVER_PACKAGE: frontend
|
||||
|
||||
docker-publish-frontend-2:
|
||||
<<: *publish-job
|
||||
environment:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
*node_modules
|
||||
packages/server/.env
|
||||
packages/server/dist
|
||||
packages/frontend/dist
|
||||
packages/frontend/profiler
|
||||
packages/viewer/dist
|
||||
packages/objectloader/dist
|
||||
packages/*/dist
|
||||
@@ -22,7 +20,6 @@ coverage/
|
||||
|
||||
packages/viewer/example/*.js
|
||||
packages/viewer/example/*.js.map
|
||||
packages/frontend/schema.graphql
|
||||
.tool-versions
|
||||
|
||||
packages/server/reports*
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
This monorepo is the home of the Speckle v2 web packages:
|
||||
|
||||
- [`packages/server`](https://github.com/specklesystems/speckle-server/blob/main/packages/server): the Server, a nodejs app. Core external dependencies are a Redis and Postgresql db.
|
||||
- [`packages/frontend`](https://github.com/specklesystems/speckle-server/blob/main/packages/frontend): the Frontend, a static Vue app.
|
||||
- [`packages/frontend-2`](https://github.com/specklesystems/speckle-server/blob/main/packages/frontend-2): the Frontend, a Nuxt/Vue app.
|
||||
- [`packages/viewer`](https://github.com/specklesystems/speckle-server/blob/main/packages/viewer): a threejs extension that allows you to display 3D data [](https://www.npmjs.com/package/@speckle/viewer)
|
||||
- [`packages/objectloader`](https://github.com/specklesystems/speckle-server/blob/main/packages/objectloader): a small js utility class that helps you stream an object and all its sub-components from the Speckle Server API. [](https://www.npmjs.com/package/@speckle/objectloader)
|
||||
- [`packages/preview-service`](https://github.com/specklesystems/speckle-server/blob/main/packages/preview-service): generates object previews for Speckle Objects headlessly. This package is meant to be called on by the server.
|
||||
|
||||
@@ -72,7 +72,6 @@ services:
|
||||
FILE_SIZE_LIMIT_MB: 100
|
||||
EMAIL_FROM: 'no-reply@example.org'
|
||||
|
||||
USE_FRONTEND_2: true
|
||||
FRONTEND_ORIGIN: 'http://127.0.0.1'
|
||||
ONBOARDING_STREAM_URL: 'https://latest.speckle.systems/projects/843d07eb10'
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ services:
|
||||
environment:
|
||||
SPECKLE_SERVER: http://127.0.0.1 # this is the canonical url
|
||||
SERVER_VERSION: 2
|
||||
FRONTEND_VERSION: '2'
|
||||
VERIFY_CERTIFICATE: '0'
|
||||
restart: 'no'
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"dev:minimal": "yarn workspaces foreach -pivW -j unlimited --include '{@speckle/server,@speckle/frontend-2}' run dev",
|
||||
"gqlgen": "yarn workspaces foreach -pivW -j unlimited --include '{@speckle/server,@speckle/frontend,@speckle/frontend-2,@speckle/dui3}' run gqlgen",
|
||||
"dev:server": "yarn workspace @speckle/server dev",
|
||||
"dev:frontend": "yarn workspace @speckle/frontend dev",
|
||||
"dev:frontend-2": "yarn workspace @speckle/frontend-2 dev",
|
||||
"dev:shared": "yarn workspace @speckle/shared dev",
|
||||
"prepare": "husky install",
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
/**
|
||||
* TODO: Does this need to change for new frontend?
|
||||
*/
|
||||
export const speckleWebAppId = 'spklwebapp'
|
||||
|
||||
export enum AuthStrategy {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
} from '~~/lib/auth/errors/errors'
|
||||
import { speckleWebAppId } from '~~/lib/auth/helpers/strategies'
|
||||
|
||||
// TODO: Should these differ from the old frontend values?
|
||||
const appId = speckleWebAppId
|
||||
const appSecret = speckleWebAppId
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
since 2019
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"name": "Launch localhost",
|
||||
"url": "http://127.0.0.1:3000",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"sourceMaps": true,
|
||||
"trace": true,
|
||||
"pathMappings": [
|
||||
{
|
||||
"url": "webpack:///src/main",
|
||||
"path": "${workspaceFolder}/src/main"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://127.0.0.1:3000",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"sourceMaps": true,
|
||||
"trace": true,
|
||||
"pathMappings": [
|
||||
{
|
||||
"url": "webpack:///src/main",
|
||||
"path": "${workspaceFolder}/src/main"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"javascript.suggest.autoImports": true,
|
||||
"typescript.suggest.autoImports": true,
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative",
|
||||
"volar.completion.preferredTagNameCase": "kebab",
|
||||
"vitest.disableWorkspaceWarning": true
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
# NOTE: Docker context should be set to git root directory, to include the viewer
|
||||
ARG NODE_ENV=production
|
||||
ARG SPECKLE_SERVER_VERSION=custom
|
||||
# build stage
|
||||
FROM node:18-bullseye-slim@sha256:8cc7dcd5aa06715247f8f2f258332f188d4221e2685b1a0159e4e6c3382e4918 as build-stage
|
||||
ARG NODE_ENV
|
||||
ARG SPECKLE_SERVER_VERSION
|
||||
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
|
||||
WORKDIR /speckle-server
|
||||
COPY .yarnrc.yml .
|
||||
COPY .yarn ./.yarn
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
# Onyl copy in the relevant package.json files for the dependencies
|
||||
COPY packages/frontend-2/type-augmentations/stubs ./packages/frontend-2/type-augmentations/stubs/
|
||||
COPY packages/frontend/package.json ./packages/frontend/
|
||||
COPY packages/viewer/package.json ./packages/viewer/
|
||||
COPY packages/objectloader/package.json ./packages/objectloader/
|
||||
COPY packages/shared/package.json ./packages/shared/
|
||||
|
||||
RUN yarn workspaces focus --all
|
||||
|
||||
# Onyl copy in the relevant source files for the dependencies
|
||||
COPY packages/objectloader ./packages/objectloader/
|
||||
COPY packages/viewer ./packages/viewer/
|
||||
COPY packages/frontend ./packages/frontend/
|
||||
COPY packages/shared ./packages/shared/
|
||||
|
||||
# This way the foreach only builds the frontend and its deps
|
||||
RUN yarn workspaces foreach -W run build
|
||||
|
||||
RUN DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get -q update && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
gettext=0.21-4 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# production stage
|
||||
FROM bitnami/openresty:1.21.4-3-debian-11-r3@sha256:456f29ba40fb4b5591ded0666c50c5026e3e0f97397440b9c5f2246813de9ec8 as production-stage
|
||||
ARG NODE_ENV
|
||||
ARG SPECKLE_SERVER_VERSION
|
||||
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
ENV FILE_SIZE_LIMIT_MB=100
|
||||
|
||||
COPY --from=build-stage /usr/bin/envsubst /usr/bin/envsubst
|
||||
|
||||
COPY --from=build-stage /speckle-server/packages/frontend/dist /app
|
||||
|
||||
COPY packages/frontend/nginx/ /opt/bitnami/openresty/nginx/
|
||||
|
||||
# prepare the environment
|
||||
ENTRYPOINT ["/opt/bitnami/openresty/nginx/docker-entrypoint.sh"]
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["/opt/bitnami/scripts/openresty/entrypoint.sh", "/opt/bitnami/scripts/openresty/run.sh"]
|
||||
@@ -1,87 +0,0 @@
|
||||
# The Speckle Frontend App
|
||||
|
||||
[](https://twitter.com/SpeckleSystems) [](https://speckle.community) [](https://speckle.systems) [](https://speckle.guide/dev/)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
We're working to stabilize the 2.0 API, and until then there will be breaking changes.
|
||||
|
||||
Notes:
|
||||
|
||||
- In **development** mode, the Speckle Server will proxy the frontend from `localhost:3000` to `localhost:8080`. If you don't see anything, ensure you've run `yarn serve` in the frontend package.
|
||||
|
||||
- In **production** mode, the Speckle Frontend will be statically served by nginx (see the Dockerfile in the current directory).
|
||||
|
||||
## Documentation
|
||||
|
||||
Comprehensive developer and user documentation can be found in our:
|
||||
|
||||
#### 📚 [Speckle Docs website](https://speckle.guide/dev/)
|
||||
|
||||
## Project setup
|
||||
|
||||
Make sure you follow the Developing and Debugging section in the project root readme.
|
||||
|
||||
### Running
|
||||
|
||||
Dev server with hot reload:
|
||||
|
||||
```
|
||||
yarn dev
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
#### TS in Vue
|
||||
|
||||
1. Set `<script lang="ts">` in your .vue SFC
|
||||
1. Make sure you do `export default defineComponent({...})` or `export default Vue.extend({...})` (or something else that is explicity typed to be a Vue component) not just `export default`, otherwise it's not clear to TS that the exported object is a Vue component
|
||||
1. If Vetur reports incorrect errors, check this out: https://vuejs.github.io/vetur/guide/FAQ.html
|
||||
|
||||
Note: If you're defining a Vue component in a non-standard way (e.g. `vueWithMixins([]).extends({...})`), make sure you add a `// @vue/component` comment right above the Vue component object definition so that ESLint shows Vue appropriate linting rules, otherwise it won't.
|
||||
|
||||
#### 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. 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
|
||||
|
||||
If you plan to package the frontend to use in a production setting, see our [Server deployment instructions](https://speckle.guide/dev/server-setup.html) (chapter `Run your speckle-server fork`)
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Vue TypeScript types get stuck in VSCode
|
||||
|
||||
Restart the Vetur Vue Language Server (VLS) through the command palette. Vetur is a bit janky and sometimes it gets stuck and isn't able to find new types/code.
|
||||
|
||||
#### Property 'xxx' does not exist on type 'CombinedVueInstance'
|
||||
|
||||
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.
|
||||
|
||||
## License
|
||||
|
||||
Unless otherwise described, the code in this repository is licensed under the Apache-2.0 License. Please note that some modules, extensions or code herein might be otherwise licensed. This is indicated either in the root of the containing folder under a different license file, or in the respective file's header. If you have any questions, don't hesitate to get in touch with us via [email](mailto:hello@speckle.systems).
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
client: {
|
||||
service: 'speckle-server',
|
||||
url: 'http://127.0.0.1:3000/graphql',
|
||||
includes: ['src/**/*.{js,jsx,ts,tsx,vue,gql}']
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: ['@vue/cli-plugin-babel/preset'],
|
||||
ignore: ['../viewer/dist', '../objectloader/dist'],
|
||||
plugins: ['lodash']
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
overwrite: true
|
||||
schema:
|
||||
- 'http://127.0.0.1:3000/graphql'
|
||||
- 'src/graphql/local-only/schema.gql'
|
||||
documents:
|
||||
- 'src/graphql/**/*.gql'
|
||||
- 'src/**/*.{ts,tsx,js,jsx}'
|
||||
- '!src/graphql/generated/**/*'
|
||||
generates:
|
||||
src/graphql/generated/graphql.ts:
|
||||
plugins:
|
||||
- 'typescript'
|
||||
- 'typescript-operations'
|
||||
- 'typescript-document-nodes'
|
||||
- 'typed-document-node'
|
||||
config:
|
||||
dedupeFragments: true
|
||||
config:
|
||||
scalars:
|
||||
JSONObject: Record<string, unknown>
|
||||
DateTime: string
|
||||
require:
|
||||
- ts-node/register
|
||||
- tsconfig-paths/register
|
||||
@@ -1,119 +0,0 @@
|
||||
import {
|
||||
baseConfigs,
|
||||
globals,
|
||||
prettierConfig,
|
||||
getESMDirname
|
||||
} from '../../eslint.config.mjs'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
const tsParserOptions = {
|
||||
tsconfigRootDir: getESMDirname(import.meta.url),
|
||||
project: './tsconfig.eslint.json',
|
||||
extraFileExtensions: ['.vue']
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Array<import('eslint').Linter.FlatConfig>}
|
||||
*/
|
||||
const configs = [
|
||||
...baseConfigs,
|
||||
{
|
||||
ignores: ['nginx/**', 'generated/**/*']
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['*.js'],
|
||||
ignores: ['vite.config.js'],
|
||||
languageOptions: {
|
||||
sourceType: 'commonjs',
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
// TS
|
||||
...tseslint.configs.recommendedTypeChecked.map((c) => ({
|
||||
...c,
|
||||
files: [...(c.files || []), '**/*.ts', '**/*.d.ts', '**/*.vue']
|
||||
})),
|
||||
{
|
||||
files: ['**/*.ts', '**/*.d.ts'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...tsParserOptions
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off'
|
||||
}
|
||||
},
|
||||
// Vue
|
||||
...pluginVue.configs['flat/vue2-recommended'].map((c) => ({
|
||||
...c,
|
||||
files: [...(c.files || []), '**/*.vue']
|
||||
})),
|
||||
{
|
||||
files: ['**/*.vue'],
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error'],
|
||||
'vue/component-name-in-template-casing': ['warn', 'kebab-case'],
|
||||
'vue/require-default-prop': 'off',
|
||||
|
||||
// TODO: Can we clean some of these up?
|
||||
// There was a lot of `any` magic in FE1, would take a lot of effort to clean those up
|
||||
'@typescript-eslint/no-unsafe-call': 'off', // can be turned on, but there's a lot of fixing to do
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off', // can be turned on, but there's a lot of fixing to do
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off', // can be turned on, but there's a lot of fixing to do
|
||||
'@typescript-eslint/no-unsafe-argument': 'off', // can be turned on, but there's a lot of fixing to do
|
||||
'@typescript-eslint/no-unsafe-return': 'off' // can be turned on, but there's a lot of fixing to do
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
...tsParserOptions
|
||||
}
|
||||
}
|
||||
},
|
||||
// Vue + TS
|
||||
{
|
||||
files: ['**/*.vue', '**/*.ts', '**/*.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/unbound-method': 'off', // too many false positives
|
||||
'@typescript-eslint/restrict-template-expressions': 'off', // too restrictive
|
||||
'@typescript-eslint/no-this-alias': 'off', // who cares lol
|
||||
'@typescript-eslint/no-misused-promises': 'off', // too restrictive
|
||||
'@typescript-eslint/no-implied-eval': 'off', // false positives cause of any
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': 'off', // too restrictive
|
||||
|
||||
'@typescript-eslint/no-floating-promises': 'off', // can be turned on, but there's a lot of fixing to do
|
||||
'@typescript-eslint/require-await': 'off', // can be turned on, but there's a lot of fixing to do
|
||||
'@typescript-eslint/await-thenable': 'off' // can be turned on, but there's a lot of fixing to do
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['./*.{js,ts}', './build-config/**/*.{js, ts}'],
|
||||
languageOptions: {
|
||||
sourceType: 'commonjs',
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.commonjs
|
||||
}
|
||||
}
|
||||
},
|
||||
prettierConfig
|
||||
]
|
||||
|
||||
export default configs
|
||||
@@ -1,142 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1.0,maximum-scale=1.0, user-scalable=0"
|
||||
/>
|
||||
<meta property="og:title" content="Speckle" />
|
||||
<meta property="og:description" content="" />
|
||||
<meta property="og:image" content="/og_image.png" />
|
||||
<meta property="og:url" content="/" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>Speckle</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #333333;
|
||||
color: #0a66ff;
|
||||
}
|
||||
@media screen and (prefers-color-scheme: light) {
|
||||
body {
|
||||
background-color: white !important;
|
||||
color: #0a66ff;
|
||||
}
|
||||
}
|
||||
|
||||
.tada {
|
||||
-webkit-animation-name: tada;
|
||||
animation-name: tada;
|
||||
-webkit-animation-duration: 1s;
|
||||
animation-duration: 1s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.hover-tada:hover {
|
||||
-webkit-animation-name: tada;
|
||||
animation-name: tada;
|
||||
-webkit-animation-duration: 1s;
|
||||
animation-duration: 1s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes tada {
|
||||
0% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
10%,
|
||||
20% {
|
||||
-webkit-transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
}
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
@keyframes tada {
|
||||
0% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
10%,
|
||||
20% {
|
||||
-webkit-transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
}
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>
|
||||
We're sorry but Speckle doesn't work properly without JavaScript enabled. Please
|
||||
enable it to continue.
|
||||
</strong>
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<div
|
||||
style="
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
font-family: sans-serif !important;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
"
|
||||
>
|
||||
<img src="/logo.svg" style="max-width: 50px" class="tada" />
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="./src/main/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
# shellcheck disable=SC2016,SC2046
|
||||
defined_envs=$(printf '${%s} ' $(env | cut -d= -f1))
|
||||
|
||||
echo Starting nginx environment template rendering with "${defined_envs}"
|
||||
|
||||
cp /opt/bitnami/openresty/nginx/mime.types /opt/bitnami/openresty/nginx/conf/mime.types
|
||||
envsubst "${defined_envs}" < /opt/bitnami/openresty/nginx/templates/nginx.conf.template > /opt/bitnami/openresty/nginx/conf/nginx.conf
|
||||
|
||||
echo Nginx conf rendered, starting server...
|
||||
exec "$@"
|
||||
@@ -1,98 +0,0 @@
|
||||
types {
|
||||
text/html html htm shtml;
|
||||
text/css css;
|
||||
text/xml xml;
|
||||
image/gif gif;
|
||||
image/jpeg jpeg jpg;
|
||||
application/javascript js;
|
||||
application/atom+xml atom;
|
||||
application/rss+xml rss;
|
||||
|
||||
text/mathml mml;
|
||||
text/plain txt;
|
||||
text/vnd.sun.j2me.app-descriptor jad;
|
||||
text/vnd.wap.wml wml;
|
||||
text/x-component htc;
|
||||
|
||||
image/avif avif;
|
||||
image/png png;
|
||||
image/svg+xml svg svgz;
|
||||
image/tiff tif tiff;
|
||||
image/vnd.wap.wbmp wbmp;
|
||||
image/webp webp;
|
||||
image/x-icon ico;
|
||||
image/x-jng jng;
|
||||
image/x-ms-bmp bmp;
|
||||
|
||||
font/woff woff;
|
||||
font/woff2 woff2;
|
||||
|
||||
application/java-archive jar war ear;
|
||||
application/json json;
|
||||
application/mac-binhex40 hqx;
|
||||
application/msword doc;
|
||||
application/pdf pdf;
|
||||
application/postscript ps eps ai;
|
||||
application/rtf rtf;
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
application/vnd.google-earth.kml+xml kml;
|
||||
application/vnd.google-earth.kmz kmz;
|
||||
application/vnd.ms-excel xls;
|
||||
application/vnd.ms-fontobject eot;
|
||||
application/vnd.ms-powerpoint ppt;
|
||||
application/vnd.oasis.opendocument.graphics odg;
|
||||
application/vnd.oasis.opendocument.presentation odp;
|
||||
application/vnd.oasis.opendocument.spreadsheet ods;
|
||||
application/vnd.oasis.opendocument.text odt;
|
||||
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||
pptx;
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
xlsx;
|
||||
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||
docx;
|
||||
application/vnd.wap.wmlc wmlc;
|
||||
application/wasm wasm;
|
||||
application/x-7z-compressed 7z;
|
||||
application/x-cocoa cco;
|
||||
application/x-java-archive-diff jardiff;
|
||||
application/x-java-jnlp-file jnlp;
|
||||
application/x-makeself run;
|
||||
application/x-perl pl pm;
|
||||
application/x-pilot prc pdb;
|
||||
application/x-rar-compressed rar;
|
||||
application/x-redhat-package-manager rpm;
|
||||
application/x-sea sea;
|
||||
application/x-shockwave-flash swf;
|
||||
application/x-stuffit sit;
|
||||
application/x-tcl tcl tk;
|
||||
application/x-x509-ca-cert der pem crt;
|
||||
application/x-xpinstall xpi;
|
||||
application/xhtml+xml xhtml;
|
||||
application/xspf+xml xspf;
|
||||
application/zip zip;
|
||||
|
||||
application/octet-stream bin exe dll;
|
||||
application/octet-stream deb;
|
||||
application/octet-stream dmg;
|
||||
application/octet-stream iso img;
|
||||
application/octet-stream msi msp msm;
|
||||
|
||||
audio/midi mid midi kar;
|
||||
audio/mpeg mp3;
|
||||
audio/ogg ogg;
|
||||
audio/x-m4a m4a;
|
||||
audio/x-realaudio ra;
|
||||
|
||||
video/3gpp 3gpp 3gp;
|
||||
video/mp2t ts;
|
||||
video/mp4 mp4;
|
||||
video/mpeg mpeg mpg;
|
||||
video/quicktime mov;
|
||||
video/webm webm;
|
||||
video/x-flv flv;
|
||||
video/x-m4v m4v;
|
||||
video/x-mng mng;
|
||||
video/x-ms-asf asx asf;
|
||||
video/x-ms-wmv wmv;
|
||||
video/x-msvideo avi;
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
pcre_jit on;
|
||||
error_log stderr info;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# move default write paths to a custom directory
|
||||
# kubernetes can mount this directory and prevent writes to the root directory
|
||||
# https://github.com/openresty/docker-openresty/issues/119
|
||||
client_body_temp_path /bitnami/openresty/nginx-client-body;
|
||||
proxy_temp_path /bitnami/openresty/nginx-proxy;
|
||||
fastcgi_temp_path /bitnami/openresty/nginx-fastcgi;
|
||||
uwsgi_temp_path /bitnami/openresty/nginx-uwsgi;
|
||||
scgi_temp_path /bitnami/openresty/nginx-scgi;
|
||||
|
||||
log_format json_combined escape=json
|
||||
'{'
|
||||
'"time_local":"$time_local",'
|
||||
'"remote_addr":"$remote_addr",'
|
||||
'"remote_user":"$remote_user",'
|
||||
'"request":"$request",'
|
||||
'"status": "$status",'
|
||||
'"body_bytes_sent":"$body_bytes_sent",'
|
||||
'"request_time":"$request_time",'
|
||||
'"http_referrer":"$http_referer",'
|
||||
'"http_user_agent":"$http_user_agent"'
|
||||
'}';
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
access_log /dev/stdout json_combined;
|
||||
|
||||
# Speckle configuration
|
||||
server_tokens off;
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_min_length 256;
|
||||
gzip_types
|
||||
application/atom+xml
|
||||
application/geo+json
|
||||
application/javascript
|
||||
application/x-javascript
|
||||
application/json
|
||||
application/ld+json
|
||||
application/manifest+json
|
||||
application/rdf+xml
|
||||
application/rss+xml
|
||||
application/xhtml+xml
|
||||
application/xml
|
||||
font/eot
|
||||
font/otf
|
||||
font/ttf
|
||||
image/svg+xml
|
||||
text/css
|
||||
text/javascript
|
||||
text/plain
|
||||
text/xml;
|
||||
|
||||
set_real_ip_from 103.21.244.0/22;
|
||||
set_real_ip_from 103.22.200.0/22;
|
||||
set_real_ip_from 103.31.4.0/22;
|
||||
set_real_ip_from 104.16.0.0/13;
|
||||
set_real_ip_from 104.24.0.0/14;
|
||||
set_real_ip_from 108.162.192.0/18;
|
||||
set_real_ip_from 131.0.72.0/22;
|
||||
set_real_ip_from 141.101.64.0/18;
|
||||
set_real_ip_from 162.158.0.0/15;
|
||||
set_real_ip_from 172.64.0.0/13;
|
||||
set_real_ip_from 173.245.48.0/20;
|
||||
set_real_ip_from 188.114.96.0/20;
|
||||
set_real_ip_from 190.93.240.0/20;
|
||||
set_real_ip_from 197.234.240.0/22;
|
||||
set_real_ip_from 198.41.128.0/17;
|
||||
set_real_ip_from 2400:cb00::/32;
|
||||
set_real_ip_from 2606:4700::/32;
|
||||
set_real_ip_from 2803:f800::/32;
|
||||
set_real_ip_from 2405:b500::/32;
|
||||
set_real_ip_from 2405:8100::/32;
|
||||
set_real_ip_from 2c0f:f248::/32;
|
||||
set_real_ip_from 2a06:98c0::/29;
|
||||
|
||||
#use any of the following two
|
||||
real_ip_header CF-Connecting-IP;
|
||||
#real_ip_header X-Forwarded-For;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
client_max_body_size 100m;
|
||||
|
||||
location / {
|
||||
root /app;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
}
|
||||
|
||||
location ~* ^/(favicon.ico|logo.svg|loadingImage.png|og_image.png) {
|
||||
root /app;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
expires 1d;
|
||||
}
|
||||
|
||||
location ~* ^/(js/.*|fonts/.*|(css/.*)|(img/.*)|(assets/.*)) {
|
||||
root /app;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
expires 1y;
|
||||
}
|
||||
|
||||
location ~ ^/streams/.* {
|
||||
default_type text/html;
|
||||
content_by_lua_block {
|
||||
local f = assert(io.open('/app/index.html', "rb"))
|
||||
local content = f:read("*all")
|
||||
f:close()
|
||||
local http_host = ngx.var.http_host
|
||||
|
||||
content = content:gsub('<meta property=og:title (.-)>', '<meta property=og:title content="Speckle Stream">')
|
||||
|
||||
local stream_id = ngx.var.uri:sub(10)
|
||||
local img_tag = '<meta property=og:image content="https://' .. http_host .. '/preview/' .. stream_id .. '?postprocess=og&ts=' .. ngx.now() .. '">'
|
||||
|
||||
content = content:gsub('<meta property=og:image (.-)>', img_tag)
|
||||
|
||||
ngx.say(content)
|
||||
}
|
||||
}
|
||||
|
||||
location ~* ^/(graphql|explorer|(auth/.*)|(objects/.*)|(preview/.*)|(api/.*)|(static/.*)) {
|
||||
resolver 127.0.0.11 valid=30s;
|
||||
set $upstream_speckle_server speckle-server;
|
||||
client_max_body_size ${FILE_SIZE_LIMIT_MB}m;
|
||||
proxy_pass http://$upstream_speckle_server:3000;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root /app;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
{
|
||||
"name": "@speckle/frontend",
|
||||
"version": "2.5.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview --port 8080",
|
||||
"profile": "vite-bundle-visualizer --output profiler/stats.html",
|
||||
"lint:eslint": "eslint .",
|
||||
"lint:ts": "vue-tsc --noEmit",
|
||||
"lint": "yarn lint:eslint && yarn lint:ts",
|
||||
"lint:ci": "yarn lint:ts",
|
||||
"gqlgen": "graphql-codegen --config codegen.yml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.0",
|
||||
"@speckle/shared": "workspace:^",
|
||||
"@speckle/viewer": "2.17.8",
|
||||
"@tiptap/core": "^2.0.0-beta.176",
|
||||
"@tiptap/extension-bold": "^2.0.0-beta.26",
|
||||
"@tiptap/extension-document": "^2.0.0-beta.15",
|
||||
"@tiptap/extension-hard-break": "^2.0.0-beta.30",
|
||||
"@tiptap/extension-history": "^2.0.0-beta.21",
|
||||
"@tiptap/extension-italic": "^2.0.0-beta.26",
|
||||
"@tiptap/extension-link": "^2.0.0-beta.38",
|
||||
"@tiptap/extension-mention": "^2.0.0-beta.97",
|
||||
"@tiptap/extension-paragraph": "^2.0.0-beta.23",
|
||||
"@tiptap/extension-placeholder": "^2.0.0-beta.48",
|
||||
"@tiptap/extension-strike": "^2.0.0-beta.27",
|
||||
"@tiptap/extension-text": "^2.0.0-beta.15",
|
||||
"@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.2.0",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"apexcharts": "^3.33.1",
|
||||
"apollo-upload-client": "^17.0.0",
|
||||
"dompurify": "^2.5.4",
|
||||
"graphql": "^15.0.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^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-histogram-slider": "^0.3.8",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-mixpanel": "1.0.7",
|
||||
"vue-router": "^3.4.9",
|
||||
"vue-timeago": "^5.1.2",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuetify": "^2.6.10",
|
||||
"vuetify-image-input": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
"@graphql-codegen/introspection": "^4.0.3",
|
||||
"@graphql-codegen/typed-document-node": "^5.0.7",
|
||||
"@graphql-codegen/typescript": "^4.0.7",
|
||||
"@graphql-codegen/typescript-document-nodes": "^4.0.7",
|
||||
"@graphql-codegen/typescript-operations": "^4.2.1",
|
||||
"@mdi/font": "^5.8.55",
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"@swc/core": "^1.2.222",
|
||||
"@types/apollo-upload-client": "^17.0.1",
|
||||
"@types/dompurify": "^2.3.3",
|
||||
"@types/lodash": "^4.14.180",
|
||||
"@types/mixpanel-browser": "^2.50.2",
|
||||
"@types/node": "^17.0.43",
|
||||
"@typescript-eslint/eslint-plugin": "^7.12.0",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"@vitejs/plugin-vue2": "^2.2.0",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"eslint": "^9.4.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"prettier": "^2.5.1",
|
||||
"sass": "~1.32.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.0.0",
|
||||
"type-fest": "^2.13.1",
|
||||
"typescript": "~4.5.5",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^5.3.4",
|
||||
"vite-bundle-visualizer": "^0.7.0",
|
||||
"vite-plugin-simple-gql": "^0.5.0",
|
||||
"vue-tsc": "^1.8.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 416 314" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-5895,-5112)">
|
||||
<g id="Artboard1" transform="matrix(1,0,0,1,4187.76,4786.57)">
|
||||
<rect x="0" y="0" width="3840" height="2160" style="fill:none;"/>
|
||||
<g transform="matrix(0.998475,-0.0552112,0,1.00153,504.711,-136.152)">
|
||||
<rect x="1294.87" y="614.156" width="204.453" height="204.719" style="fill:rgb(4,126,251);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.989814,-0.0547323,0.524518,0.471524,802.293,120.929)">
|
||||
<rect x="643.94" y="618.105" width="206.242" height="64.29" style="fill:rgb(123,188,255);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.362077,0.325495,0,1.34467,1135,426.883)">
|
||||
<rect x="1736.88" y="-457.431" width="93.132" height="152.477" style="fill:rgb(49,59,207);"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB |
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 416 314" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-5895,-5112)">
|
||||
<g id="Artboard1" transform="matrix(1,0,0,1,4187.76,4786.57)">
|
||||
<rect x="0" y="0" width="3840" height="2160" style="fill:none;"/>
|
||||
<g transform="matrix(0.998475,-0.0552112,0,1.00153,504.711,-136.152)">
|
||||
<rect x="1294.87" y="614.156" width="204.453" height="204.719" style="fill:rgb(4,126,251);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.989814,-0.0547323,0.524518,0.471524,802.293,120.929)">
|
||||
<rect x="643.94" y="618.105" width="206.242" height="64.29" style="fill:rgb(123,188,255);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.362077,0.325495,0,1.34467,1135,426.883)">
|
||||
<rect x="1736.88" y="-457.431" width="93.132" height="152.477" style="fill:rgb(49,59,207);"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 3.7 MiB |
|
Before Width: | Height: | Size: 503 KiB |
|
Before Width: | Height: | Size: 646 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
@@ -1,249 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 997 769" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-3958,-1864)">
|
||||
<g id="devs" transform="matrix(1,0,0,1,2934.11,-10.551)">
|
||||
<rect x="1024" y="1875.4" width="995.978" height="768" style="fill:none;"/>
|
||||
<g transform="matrix(1,0,0,1,14.9065,16.6081)">
|
||||
<g transform="matrix(1,0,0,1,-29.4313,15.2744)">
|
||||
<g transform="matrix(0.556609,0,0,0.556609,737.492,974.014)">
|
||||
<path d="M1564.47,2104.14L1554.7,2098.47L1554.7,2148.03L1554.7,2148.03C1554.7,2151.55 1557.27,2155.27 1562.29,2158.16L1695.31,2234.96C1700.08,2237.72 1706.12,2239.19 1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34C1717.33,2239.47 1722.52,2238.46 1726.34,2236.25L1863.82,2156.88C1867.64,2154.67 1869.39,2151.67 1869.16,2148.56L1869.18,2148.55L1869.18,2099.6L1859.32,2104.09L1728.57,2028.6C1719.39,2023.3 1705.49,2022.73 1697.54,2027.31L1564.47,2104.14Z" style="fill:url(#_Radial1);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.482727,-0.278703,0.557405,0.321818,736.148,973.087)">
|
||||
<path d="M-1014.61,2607.25C-1014.61,2598.07 -1022.06,2590.62 -1031.24,2590.62L-1189.98,2590.62C-1199.16,2590.62 -1206.61,2598.07 -1206.61,2607.25L-1206.61,2740.27C-1206.61,2749.45 -1199.16,2756.9 -1189.98,2756.9L-1031.24,2756.9C-1022.06,2756.9 -1014.61,2749.45 -1014.61,2740.27L-1014.61,2607.25Z" style="fill:rgb(241,243,248);"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,-70.5477)">
|
||||
<g transform="matrix(0.556609,0,0,0.556609,558.017,1301.2)">
|
||||
<path d="M1564.47,2104.14L1554.7,2098.47L1554.7,2148.03L1554.7,2148.03C1554.7,2151.55 1557.27,2155.27 1562.29,2158.16L1695.31,2234.96C1700.08,2237.72 1706.12,2239.19 1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34C1717.33,2239.47 1722.52,2238.46 1726.34,2236.25L1863.82,2156.88C1867.64,2154.67 1869.39,2151.67 1869.16,2148.56L1869.18,2148.55L1869.18,2099.6L1859.32,2104.09L1728.57,2028.6C1719.39,2023.3 1705.49,2022.73 1697.54,2027.31L1564.47,2104.14Z" style="fill:url(#_Radial2);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.482727,-0.278703,0.557405,0.321818,556.673,1300.27)">
|
||||
<path d="M-1014.61,2607.25C-1014.61,2598.07 -1022.06,2590.62 -1031.24,2590.62L-1189.98,2590.62C-1199.16,2590.62 -1206.61,2598.07 -1206.61,2607.25L-1206.61,2740.27C-1206.61,2749.45 -1199.16,2756.9 -1189.98,2756.9L-1031.24,2756.9C-1022.06,2756.9 -1014.61,2749.45 -1014.61,2740.27L-1014.61,2607.25Z" style="fill:rgb(241,243,248);"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,-70.5477)">
|
||||
<g transform="matrix(0.556609,0,0,0.556609,558.017,1252.14)">
|
||||
<path d="M1564.47,2104.14L1554.7,2098.47L1554.7,2148.03L1554.7,2148.03C1554.7,2151.55 1557.27,2155.27 1562.29,2158.16L1695.31,2234.96C1700.08,2237.72 1706.12,2239.19 1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34C1717.33,2239.47 1722.52,2238.46 1726.34,2236.25L1863.82,2156.88C1867.64,2154.67 1869.39,2151.67 1869.16,2148.56L1869.18,2148.55L1869.18,2099.6L1859.32,2104.09L1728.57,2028.6C1719.39,2023.3 1705.49,2022.73 1697.54,2027.31L1564.47,2104.14Z" style="fill:url(#_Radial3);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.482727,-0.278703,0.557405,0.321818,556.673,1251.22)">
|
||||
<path d="M-1014.61,2607.25C-1014.61,2598.07 -1022.06,2590.62 -1031.24,2590.62L-1189.98,2590.62C-1199.16,2590.62 -1206.61,2598.07 -1206.61,2607.25L-1206.61,2740.27C-1206.61,2749.45 -1199.16,2756.9 -1189.98,2756.9L-1031.24,2756.9C-1022.06,2756.9 -1014.61,2749.45 -1014.61,2740.27L-1014.61,2607.25Z" style="fill:rgb(241,243,248);"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,0.590914,-4.54747e-13,927.132)">
|
||||
<g transform="matrix(4.41926e-17,0.721719,-0.187381,1.14738e-17,1917.43,-929.382)">
|
||||
<clipPath id="_clip4">
|
||||
<rect x="4427.94" y="2248.85" width="214.08" height="68.028"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip4)">
|
||||
<g transform="matrix(8.48423e-17,-5.33673,2.34481,5.53009e-16,3470.31,4848.15)">
|
||||
<use xlink:href="#_Image5" x="483.725" y="411.539" width="12.747px" height="91.299px" transform="matrix(0.980542,0,0,0.992383,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(4.41926e-17,0.721719,-0.187381,1.14738e-17,1938.81,-929.382)">
|
||||
<clipPath id="_clip6">
|
||||
<rect x="4427.94" y="2248.85" width="214.08" height="68.028"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip6)">
|
||||
<g transform="matrix(8.48423e-17,-5.33673,2.34481,5.53009e-16,3470.31,4962.25)">
|
||||
<use xlink:href="#_Image5" x="505.529" y="411.539" width="12.747px" height="91.299px" transform="matrix(0.980542,0,0,0.992383,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(4.41926e-17,0.721719,-0.187381,1.14738e-17,1963.76,-929.382)">
|
||||
<clipPath id="_clip7">
|
||||
<rect x="4427.94" y="2248.85" width="214.08" height="68.028"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip7)">
|
||||
<g transform="matrix(8.48423e-17,-5.33673,2.34481,5.53009e-16,3470.31,5095.41)">
|
||||
<use xlink:href="#_Image5" x="530.975" y="411.539" width="12.747px" height="91.299px" transform="matrix(0.980542,0,0,0.992383,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.785519,0,0,0.785519,-1990.09,499.836)">
|
||||
<g transform="matrix(0.708588,0,0,0.708588,3243.86,732.695)">
|
||||
<path d="M1564.47,2104.14L1554.7,2098.47L1554.7,2148.03L1554.7,2148.03C1554.7,2151.55 1557.27,2155.27 1562.29,2158.16L1695.31,2234.96C1700.08,2237.72 1706.12,2239.19 1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34C1717.33,2239.47 1722.52,2238.46 1726.34,2236.25L1863.82,2156.88C1867.64,2154.67 1869.39,2151.67 1869.16,2148.56L1869.18,2148.55L1869.18,2099.6L1859.32,2104.09L1728.57,2028.6C1719.39,2023.3 1705.49,2022.73 1697.54,2027.31L1564.47,2104.14Z" style="fill:url(#_Radial8);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.614533,-0.354801,0.709601,0.409689,3242.15,731.515)">
|
||||
<path d="M-1014.61,2607.25C-1014.61,2598.07 -1022.06,2590.62 -1031.24,2590.62L-1189.98,2590.62C-1199.16,2590.62 -1206.61,2598.07 -1206.61,2607.25L-1206.61,2740.27C-1206.61,2749.45 -1199.16,2756.9 -1189.98,2756.9L-1031.24,2756.9C-1022.06,2756.9 -1014.61,2749.45 -1014.61,2740.27L-1014.61,2607.25Z" style="fill:rgb(37,99,235);"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(3.36792,-1.94447,0.544272,0.314236,-1836.01,1629.53)">
|
||||
<clipPath id="_clip9">
|
||||
<rect x="348.837" y="4017.54" width="23.533" height="75.348"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip9)">
|
||||
<g transform="matrix(0.14846,0.918658,-0.25714,1.59116,363.633,2977.02)">
|
||||
<use xlink:href="#_Image10" x="519.642" y="312.497" width="120.267px" height="69.436px" transform="matrix(0.993944,0,0,0.991947,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,10)">
|
||||
<g transform="matrix(-1.54203,-0.890293,0.104003,-0.060046,1564.74,2738.08)">
|
||||
<clipPath id="_clip11">
|
||||
<rect x="317.463" y="4041.6" width="57.199" height="89.822"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip11)">
|
||||
<g transform="matrix(-0.324247,4.80757,-0.561613,-8.32695,680.115,4740.3)">
|
||||
<use xlink:href="#_Image12" x="400.21" y="312.242" width="97.545px" height="56.317px" transform="matrix(0.995354,0,0,0.988025,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(-1.54203,-0.890293,0.104003,-0.060046,1544.61,2749.7)">
|
||||
<clipPath id="_clip13">
|
||||
<rect x="317.463" y="4041.6" width="57.199" height="89.822"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip13)">
|
||||
<g transform="matrix(-0.324247,4.80757,-0.561613,-8.32695,680.115,4933.82)">
|
||||
<use xlink:href="#_Image12" x="379.99" y="324.003" width="97.545px" height="56.317px" transform="matrix(0.995354,0,0,0.988025,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(-1.54203,-0.890293,0.104003,-0.060046,1522.97,2762.2)">
|
||||
<clipPath id="_clip14">
|
||||
<rect x="317.463" y="4041.6" width="57.199" height="89.822"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip14)">
|
||||
<g transform="matrix(-0.324247,4.80757,-0.561613,-8.32695,680.115,5141.93)">
|
||||
<use xlink:href="#_Image12" x="358.245" y="336.651" width="97.545px" height="56.317px" transform="matrix(0.995354,0,0,0.988025,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(-0.830211,-9.20644e-17,2.77667e-16,-0.839612,2754.97,4122.21)">
|
||||
<g transform="matrix(-1.54203,-0.890293,0.104003,-0.060046,1570.11,2734.48)">
|
||||
<clipPath id="_clip15">
|
||||
<rect x="317.463" y="4041.6" width="57.199" height="89.822"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip15)">
|
||||
<g transform="matrix(0.39056,-5.79078,0.668896,9.91761,-151.653,2875.93)">
|
||||
<use xlink:href="#_Image16" x="492.27" y="415.625" width="80.983px" height="47.285px" transform="matrix(0.999786,0,0,0.9851,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(-1.54203,-0.890293,0.104003,-0.060046,1508.59,2769.81)">
|
||||
<clipPath id="_clip17">
|
||||
<rect x="317.463" y="4041.6" width="57.199" height="89.822"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip17)">
|
||||
<g transform="matrix(0.39056,-5.79078,0.668896,9.91761,-151.757,3465.86)">
|
||||
<use xlink:href="#_Image16" x="543.351" y="385.513" width="80.983px" height="47.285px" transform="matrix(0.999786,0,0,0.9851,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.785519,0,0,0.597818,284.221,915.242)">
|
||||
<g transform="matrix(5.62591e-17,0.91878,-0.238544,1.46066e-17,2264.17,-2166.43)">
|
||||
<clipPath id="_clip18">
|
||||
<rect x="4427.94" y="2248.85" width="214.08" height="68.028"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip18)">
|
||||
<g transform="matrix(8.48423e-17,-5.33673,1.82062,4.29382e-16,4074.24,5623.79)">
|
||||
<use xlink:href="#_Image19" x="631.949" y="194.96" width="12.747px" height="117.586px" transform="matrix(0.980542,0,0,0.996492,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(5.62591e-17,0.91878,-0.238544,1.46066e-17,2291.39,-2166.43)">
|
||||
<clipPath id="_clip20">
|
||||
<rect x="4427.94" y="2248.85" width="214.08" height="68.028"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip20)">
|
||||
<g transform="matrix(8.48423e-17,-5.33673,1.82062,4.29382e-16,4074.24,5737.89)">
|
||||
<use xlink:href="#_Image19" x="653.754" y="194.96" width="12.747px" height="117.586px" transform="matrix(0.980542,0,0,0.996492,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(5.62591e-17,0.91878,-0.238544,1.46066e-17,2323.16,-2166.43)">
|
||||
<clipPath id="_clip21">
|
||||
<rect x="4427.94" y="2248.85" width="214.08" height="68.028"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip21)">
|
||||
<g transform="matrix(8.48423e-17,-5.33673,1.82062,4.29382e-16,4074.24,5871.05)">
|
||||
<use xlink:href="#_Image19" x="679.2" y="194.96" width="12.747px" height="117.586px" transform="matrix(0.980542,0,0,0.996492,0,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1.50894,0,0,1.50894,-984.123,-1457.54)">
|
||||
<g>
|
||||
<g transform="matrix(0.368875,0,0,0.368875,1121.71,1555.77)">
|
||||
<path d="M1564.47,2104.14L1554.7,2098.47L1554.7,2148.03L1554.7,2148.03C1554.7,2151.55 1557.27,2155.27 1562.29,2158.16L1695.31,2234.96C1700.08,2237.72 1706.12,2239.19 1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34C1717.33,2239.47 1722.52,2238.46 1726.34,2236.25L1863.82,2156.88C1867.64,2154.67 1869.39,2151.67 1869.16,2148.56L1869.18,2148.55L1869.18,2099.6L1859.32,2104.09L1728.57,2028.6C1719.39,2023.3 1705.49,2022.73 1697.54,2027.31L1564.47,2104.14Z" style="fill:url(#_Radial22);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.319912,-0.184701,0.369403,0.213275,1120.82,1555.16)">
|
||||
<path d="M-1014.61,2607.25C-1014.61,2598.07 -1022.06,2590.62 -1031.24,2590.62L-1189.98,2590.62C-1199.16,2590.62 -1206.61,2598.07 -1206.61,2607.25L-1206.61,2740.27C-1206.61,2749.45 -1199.16,2756.9 -1189.98,2756.9L-1031.24,2756.9C-1022.06,2756.9 -1014.61,2749.45 -1014.61,2740.27L-1014.61,2607.25Z" style="fill:rgb(241,243,248);"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.785519,0,0,0.785519,-1990.09,460.263)">
|
||||
<g transform="matrix(0.708588,0,0,0.708588,3243.86,732.695)">
|
||||
<path d="M1564.47,2104.14L1554.7,2098.47L1554.7,2148.03L1554.7,2148.03C1554.7,2151.55 1557.27,2155.27 1562.29,2158.16L1695.31,2234.96C1700.08,2237.72 1706.12,2239.19 1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34C1717.33,2239.47 1722.52,2238.46 1726.34,2236.25L1863.82,2156.88C1867.64,2154.67 1869.39,2151.67 1869.16,2148.56L1869.18,2148.55L1869.18,2099.6L1859.32,2104.09L1728.57,2028.6C1719.39,2023.3 1705.49,2022.73 1697.54,2027.31L1564.47,2104.14Z" style="fill:url(#_Radial23);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.614533,-0.354801,0.709601,0.409689,3242.15,731.515)">
|
||||
<path d="M-1014.61,2607.25C-1014.61,2598.07 -1022.06,2590.62 -1031.24,2590.62L-1189.98,2590.62C-1199.16,2590.62 -1206.61,2598.07 -1206.61,2607.25L-1206.61,2740.27C-1206.61,2749.45 -1199.16,2756.9 -1189.98,2756.9L-1031.24,2756.9C-1022.06,2756.9 -1014.61,2749.45 -1014.61,2740.27L-1014.61,2607.25Z" style="fill:rgb(37,99,235);"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1.50894,0,0,1.50894,-1292.7,-1350.8)">
|
||||
<g transform="matrix(0.368875,0,0,0.368875,1121.71,1555.77)">
|
||||
<path d="M1564.47,2104.14L1554.7,2098.47L1554.7,2148.03L1554.7,2148.03C1554.7,2151.55 1557.27,2155.27 1562.29,2158.16L1695.31,2234.96C1700.08,2237.72 1706.12,2239.19 1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34C1717.33,2239.47 1722.52,2238.46 1726.34,2236.25L1863.82,2156.88C1867.64,2154.67 1869.39,2151.67 1869.16,2148.56L1869.18,2148.55L1869.18,2099.6L1859.32,2104.09L1728.57,2028.6C1719.39,2023.3 1705.49,2022.73 1697.54,2027.31L1564.47,2104.14Z" style="fill:url(#_Radial24);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.319912,-0.184701,0.369403,0.213275,1120.82,1555.16)">
|
||||
<path d="M-1014.61,2607.25C-1014.61,2598.07 -1022.06,2590.62 -1031.24,2590.62L-1189.98,2590.62C-1199.16,2590.62 -1206.61,2598.07 -1206.61,2607.25L-1206.61,2740.27C-1206.61,2749.45 -1199.16,2756.9 -1189.98,2756.9L-1031.24,2756.9C-1022.06,2756.9 -1014.61,2749.45 -1014.61,2740.27L-1014.61,2607.25Z" style="fill:rgb(241,243,248);"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-16.3136,-12.923)">
|
||||
<g transform="matrix(0.700752,0,0,0.700752,386.322,699.249)">
|
||||
<g transform="matrix(0.368875,0,0,0.368875,1121.71,1555.77)">
|
||||
<path d="M1564.47,2104.14L1554.7,2098.47L1554.7,2148.03L1554.7,2148.03C1554.7,2151.55 1557.27,2155.27 1562.29,2158.16L1695.31,2234.96C1700.08,2237.72 1706.12,2239.19 1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34C1717.33,2239.47 1722.52,2238.46 1726.34,2236.25L1863.82,2156.88C1867.64,2154.67 1869.39,2151.67 1869.16,2148.56L1869.18,2148.55L1869.18,2099.6L1859.32,2104.09L1728.57,2028.6C1719.39,2023.3 1705.49,2022.73 1697.54,2027.31L1564.47,2104.14Z" style="fill:url(#_Radial25);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.319912,-0.184701,0.369403,0.213275,1120.82,1555.16)">
|
||||
<path d="M-1014.61,2607.25C-1014.61,2598.07 -1022.06,2590.62 -1031.24,2590.62L-1189.98,2590.62C-1199.16,2590.62 -1206.61,2598.07 -1206.61,2607.25L-1206.61,2740.27C-1206.61,2749.45 -1199.16,2756.9 -1189.98,2756.9L-1031.24,2756.9C-1022.06,2756.9 -1014.61,2749.45 -1014.61,2740.27L-1014.61,2607.25Z" style="fill:rgb(241,243,248);"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.700752,0,0,0.700752,436.68,670.174)">
|
||||
<g transform="matrix(0.368875,0,0,0.368875,1121.71,1555.77)">
|
||||
<path d="M1564.47,2104.14L1554.7,2098.47L1554.7,2148.03L1554.7,2148.03C1554.7,2151.55 1557.27,2155.27 1562.29,2158.16L1695.31,2234.96C1700.08,2237.72 1706.12,2239.19 1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34C1717.33,2239.47 1722.52,2238.46 1726.34,2236.25L1863.82,2156.88C1867.64,2154.67 1869.39,2151.67 1869.16,2148.56L1869.18,2148.55L1869.18,2099.6L1859.32,2104.09L1728.57,2028.6C1719.39,2023.3 1705.49,2022.73 1697.54,2027.31L1564.47,2104.14Z" style="fill:url(#_Radial26);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.319912,-0.184701,0.369403,0.213275,1120.82,1555.16)">
|
||||
<path d="M-1014.61,2607.25C-1014.61,2598.07 -1022.06,2590.62 -1031.24,2590.62L-1189.98,2590.62C-1199.16,2590.62 -1206.61,2598.07 -1206.61,2607.25L-1206.61,2740.27C-1206.61,2749.45 -1199.16,2756.9 -1189.98,2756.9L-1031.24,2756.9C-1022.06,2756.9 -1014.61,2749.45 -1014.61,2740.27L-1014.61,2607.25Z" style="fill:rgb(241,243,248);"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.700752,0,0,0.700752,492.398,702.343)">
|
||||
<g transform="matrix(0.368875,0,0,0.368875,1121.71,1555.77)">
|
||||
<path d="M1564.47,2104.14L1554.7,2098.47L1554.7,2148.03L1554.7,2148.03C1554.7,2151.55 1557.27,2155.27 1562.29,2158.16L1695.31,2234.96C1700.08,2237.72 1706.12,2239.19 1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34C1717.33,2239.47 1722.52,2238.46 1726.34,2236.25L1863.82,2156.88C1867.64,2154.67 1869.39,2151.67 1869.16,2148.56L1869.18,2148.55L1869.18,2099.6L1859.32,2104.09L1728.57,2028.6C1719.39,2023.3 1705.49,2022.73 1697.54,2027.31L1564.47,2104.14Z" style="fill:url(#_Radial27);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.319912,-0.184701,0.369403,0.213275,1120.82,1555.16)">
|
||||
<path d="M-1014.61,2607.25C-1014.61,2598.07 -1022.06,2590.62 -1031.24,2590.62L-1189.98,2590.62C-1199.16,2590.62 -1206.61,2598.07 -1206.61,2607.25L-1206.61,2740.27C-1206.61,2749.45 -1199.16,2756.9 -1189.98,2756.9L-1031.24,2756.9C-1022.06,2756.9 -1014.61,2749.45 -1014.61,2740.27L-1014.61,2607.25Z" style="fill:rgb(241,243,248);"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.700752,0,0,0.700752,438.856,733.255)">
|
||||
<g transform="matrix(0.368875,0,0,0.368875,1121.71,1555.77)">
|
||||
<path d="M1564.47,2104.14L1554.7,2098.47L1554.7,2148.03L1554.7,2148.03C1554.7,2151.55 1557.27,2155.27 1562.29,2158.16L1695.31,2234.96C1700.08,2237.72 1706.12,2239.19 1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34L1711.94,2239.34C1717.33,2239.47 1722.52,2238.46 1726.34,2236.25L1863.82,2156.88C1867.64,2154.67 1869.39,2151.67 1869.16,2148.56L1869.18,2148.55L1869.18,2099.6L1859.32,2104.09L1728.57,2028.6C1719.39,2023.3 1705.49,2022.73 1697.54,2027.31L1564.47,2104.14Z" style="fill:url(#_Radial28);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.319912,-0.184701,0.369403,0.213275,1120.82,1555.16)">
|
||||
<path d="M-1014.61,2607.25C-1014.61,2598.07 -1022.06,2590.62 -1031.24,2590.62L-1189.98,2590.62C-1199.16,2590.62 -1206.61,2598.07 -1206.61,2607.25L-1206.61,2740.27C-1206.61,2749.45 -1199.16,2756.9 -1189.98,2756.9L-1031.24,2756.9C-1022.06,2756.9 -1014.61,2749.45 -1014.61,2740.27L-1014.61,2607.25Z" style="fill:rgb(241,243,248);"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="_Radial1" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(139.128,29.5437,-60.595,223.638,1611.84,2160.46)"><stop offset="0" style="stop-color:rgb(224,235,247);stop-opacity:1"/><stop offset="0.57" style="stop-color:rgb(203,221,240);stop-opacity:1"/><stop offset="0.79" style="stop-color:rgb(159,193,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,166,214);stop-opacity:1"/></radialGradient>
|
||||
<radialGradient id="_Radial2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(139.128,29.5437,-60.595,223.638,1611.84,2160.46)"><stop offset="0" style="stop-color:rgb(224,235,247);stop-opacity:1"/><stop offset="0.57" style="stop-color:rgb(203,221,240);stop-opacity:1"/><stop offset="0.79" style="stop-color:rgb(159,193,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,166,214);stop-opacity:1"/></radialGradient>
|
||||
<radialGradient id="_Radial3" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(139.128,29.5437,-60.595,223.638,1611.84,2160.46)"><stop offset="0" style="stop-color:rgb(224,235,247);stop-opacity:1"/><stop offset="0.57" style="stop-color:rgb(203,221,240);stop-opacity:1"/><stop offset="0.79" style="stop-color:rgb(159,193,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,166,214);stop-opacity:1"/></radialGradient>
|
||||
<image id="_Image5" width="13px" height="92px" xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCABcAA0DAREAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAwUABv/EABsQAAICAwEAAAAAAAAAAAAAAAAEAQIhMWED/8QAGQEAAwEBAQAAAAAAAAAAAAAAAgMEAQAH/8QAGREBAQEBAQEAAAAAAAAAAAAAABIBAwIT/9oADAMBAAIRAxEAPwDjD3154xzj18JkXvsUkhfGjLbKnRbhLvQ2S1WxoDegpVKrcJd6GyWFsaBtsqdV+Eu9DpLC+NAW2VOi3CXehslqtjQP0FKnVbhLvs2SwtjQO9GyqUX4S77OkkLY0BvRsqdV+Eu9DZLC2NA/QUqlVuEu9DZJVbGgN6NlUqvwm32dJI8IAtsnAExzn//Z"/>
|
||||
<radialGradient id="_Radial8" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(139.128,29.5437,-60.595,223.638,1611.84,2160.46)"><stop offset="0" style="stop-color:rgb(224,235,247);stop-opacity:1"/><stop offset="0.57" style="stop-color:rgb(203,221,240);stop-opacity:1"/><stop offset="0.79" style="stop-color:rgb(159,193,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,166,214);stop-opacity:1"/></radialGradient>
|
||||
<image id="_Image10" width="121px" height="70px" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHkAAABGCAYAAADo1jsxAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAJA0lEQVR4nO2cS4geRRCAqzoe4iOJRs1B9JBDFCGHCKJCDhLEgKhgCB4kiEp8kYARgwYiURRziCJBomgIGlARIqiICgYkiHqISBDMwaAiKIJCdpPdvC877WG6Z/pR/ZiZnvln/v0Lln+Zranqqa+ruufvYhEmUllmT1/gIZ0lixZiF2OJkd4MZAgSA9eUPsAe+QCGIHXgmjJK2GxUjoci0zMXeJYB8IaYU0yUujLJZIf8O3WeMwRgiIAsDxRDABQ/TaTrrJ5ANuTv/85zRADGcqgA+aeEzTAP2pBgTyAL+eOfPHNNkBI2oszm4cGeQAaAY3+d44yhArKEXWSzD7YCvK60CXpeQz7657m8NGuZm8MurnlgM0RNp6+w5yXkI7+d41pWMijBFtfcsBF1fQo2axjZlLDnHeQffz3LmUg1ZkAx4ZmwGQIABmBb15qNNwXseQP5+6NnuQnGBYW+5oet35cWdlPQYw/50M9nORVwHVR92P77+gF7bCF//dMZbkKh4FGwydJtTRIMlG4btqlXV6rCHkvIXx4+w+2Ah2GHoND3jQZ2FdBjBfmzH85w/1pJw3ZvqJrBdlcEen2vIzGwxwLygW9Pi3UXIzOq1HWXbve6GoLddDNXR3ywBw35g29OW2W5Wvl0w7bvC8NOvZmrIxTswULef9AGXL986lWgzruwuyI038xVFRP04CDv/eoUV6GkLZ/pYVPrsHWfOMpsC/ZgIO/5/BSns9INO6p8GmfFMbB1KLRdJtoxvLBN3y3BXpAKQpuy+9NTRVcFFz9gfHIxXzkAAOadHGorRlb8hrqOUOKo/E7Y1XQ45Ye2CwiQKQPJdZD2TdoVtlHvTqkCu9eZ/OrHs7zIKGdmqFmJgcxQswdJHSqzmdjy2jYUP+i3q+vI+9Dv29G4oOrEwO4l5Fc+muU2KDdsG5QNWw9WHGyGJigbtqbjgK35joRt2qVgqzo+2L2C/OL7s9wOlhu2W6eE7Q+WA7Zq1wHbq6MeXUJF2AG7RQbLMUfA7s2a/Pz+Gc7FCIv1CsW6aKxXAAAZF3906GjXCLu5DSgWPi6ucY60byV4xX2IpE5xrTSv2dVtqDpyfLTd4j6pX+igpgNQwl6yaGGdvVpaeXbfDLezx87AMjPMrNQz210+3ZlNZw/Svs1OESUDTZ3CriezaR09s52+A10qVywe8SvUlndmuB0sBxRvIORDoVeHsksFy4Ltsmt2ikA12FTjguUnYFfdfKmwly0d8Zchm948yamZR8EuH8oN2wYVhm3apTPDBGXbZQhgvceCH7YJJmWXyrXLLiZ5dgr58TdywHqwbNjOzFVg28HKfXhhB+zSmWHDJn2HYFN2DdghnfJTh738GhqulE4gP/z6SR4uYegJFv2AsbDdmWGCauMUyx6zaVe/rxrs66/zAwZoGfKGXSc4CQV8JYyGTQU5BDsExTlJDNjkJAnADmWg27cO2+V75fJLotm1Avn+nSe4Dcr/gClhkxWhBuyYMZt2/b7rTFBd96YV8XClJIe87uUT3BywCdsfLBp22vLph91sgnoONhpM0FtvvLQ2q4sSsYW7X5jmiABzWTlgjgCZMnCOAIhcfGKho+qjcQ2QCxvo1HHZtX2r99n61H2Z0M3/hg7ftt1MqUS+MUu7bt8Aq1fWBwyQIJPXbp+2Wl7NMpOqfIICu+vyWacaNe1SWbOqGVwpjYys2TbNfQE3d4pdrpWpyqftu86Ybdi+SbL25suSwJVSq1yv3jrFGSJkma8UAXDkBWxXKSpLJV3G9dLdXfl0+/YvEZTdTNgFEQfXmO+5LS1cKZWM3rJlquirql4+6cwOZWDK8mlmdtXyGa4I9btU7lvdDmCAyExetXmKM8w3VeXslAMvZzM1O12ZTWcGvTGyM6MMWvXNHBe+0eM74WaO5SdFehXjBez1t7cHV0rQwconj3NnHxMxq30ZaF+LfRe27aZa38HIbOtMmrCboktlwx2LWocrxenohkePcwpKXPmsBztcPsHqooiC7WzWU+4LQNGvNetSeejO7gADEJBXbDyu/SsifXbKYMXDtlpbCJBlsDywrf6nMGwTCt2/VT6nEzaavm3Ybp3S7mN3dQtXSuHUhKspoTngMOwQFCoQFOxgox0B293qWsJm6PJdQtF0HCAZ5mH0wd507+KRwJWCAH7AmnIEbDtYBJTArC+DVQ02w5Bvw64DtjY+B2zzGgX76XWjhStlQSxgKVY/EsgeJKT7ja0eKkcfk7gHwO5/4qKfy9UvbdrVbJA6hl0Fhe0b6D4xKhbF+BG2ru8HYICaX4bIxm8E8TknXo8YABOwMy5mN1N1RPYAAJ/jRQZykNfEq4aoBtmczC6e2xDNcDJ7XHYzYRfApSNt5HZZlo8hUzoqS9/Shng9YpjbyMTzy8wWdrc9sKQ3cKU0OqCQMxiRhg1ABcsIeMZFsFALlg82A1RA0XYh45BBWcZp3xKUmHARsFkmJpy0K2zseLB/cKUkOYUyYbMMgAMXwaJh5zrluhgD24JCwJZ2C1AEbNt3GLaVuQL2zkcu7y1cKcmOGgFK2MU3QARsO1hh2JCBAOWAAui0q8K2vpOOgG19M6f42bWx/4ABiq1C/A67knEZTPFjfteNQOxWtbYg4hQLStj26xuSdu136GYtSbufGAZcKdZgewHbCHS1g41Sn3p/D8L22H1r8xWDgiuFHHQboAHCsKkgm7BtUGlgh3zvfWqYgAEckKV0BRuNQMfAo2C7QXm+mdP0bdjvPTNcuFKiHqAt2HRWtnOwYcL2VwSED58bPlwplR6kS9jV/ttOhVMsYn1XfR/YvnRs4Eqp/EBtgQaIg+JfY5vB/mTH+AEGqAFZylBgx2zmvnhpPOFKafxwXcAm34UDsGPW94M7rxxruFKSPGSboAGqwY7ZzB3aNT/gSkn6sKOEHX4XBmAM4bvX5hdggMSQpXQKO7JL5fDuq+YdXCmtPnibsBEhqkvlyJ75C1dK6wFoO6sp2PK/F/zy9gQwQAeQpXQCWwA/tu/qCVxFOg9Gm7B/f3cCl5KRBSUl7Alcv4w0OClATwCHpRcBqgN7AjdeehWoGNgTuNXlf9nOIZUXHcgTAAAAAElFTkSuQmCC"/>
|
||||
<image id="_Image12" width="98px" height="57px" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGIAAAA5CAYAAADN7P46AAAACXBIWXMAAA7EAAAOxAGVKw4bAAADd0lEQVR4nO2ZPWgUQRTH31n5FeMn2GgjiiCkCYIgWsVCFBtTKQqigkQEMTYGkYg2CQgqIkSRIDZqoxaiVmohok2wiUVAbMTiPnKXyxfIzbPITW52b3ZuZm/2dnb3/eBYjrk7Zua38/5zs7lKdRHBgO6ulTmTzxN65ExFcEiIXVaE/WJYgYSc0CtChFZH+1gRwSEh4bEqgkNCzAmdEX4QARgD+FcDyJcoP0xZvnPDrgxEAMZfDIAh1q9L77dvXUWrQwPPJJnIYLgkocb4FT3vG2KWXru2kRAV0slRCREnWCaghgDI6ldJe8+O1SREgnJSRCGeO5yhrxwFtDP5Z/buJhl+tCakWF7E5frfNOkB+SCV5M2PAz1rSEgd7Yn4W1jARh4E5ENTbujlx6FeEmI8Ab/+LGDjLg9eIab5cXTf2kzLCD34yd/zqCxRIfOj/2A2hbQ16ImpefTe8RhQmgAQUbEyfO0M4FRfV6aEWBns18k5lK8MX/lStftWBhd2/nA2hFgd5MeJOa38UAmQtV88ti71MiIZ4Lvvs+r8ECfcID8G+9MrJLKBvf4y25wfTbkgz4fmrbC3/fqJ7tQJiXxAzz9V9fJDZ4cl5MfN0+mS0bHBjH+oYmPHhIF//kzzY/Tc+lQI6eggxt7OYPOprWKLq1vCGMD9gWQLiaXz917NeMM86IxKVaJk4c8AHl/ekEghsXZ65GXFXn4In3l6NXkynOjw8LOKVn7w8hS4xfUJejG00Ynx6eBMR4fGy2b50eKIRLy+GXZfiHMdvDJWbj8/JN99f3uTc2MVcbZzAw+m9Z5/SP8kBhw+IsDnUTeFONkpkTN3pvXOr+o7qCABfkHf7m52auxOdSaIkyMlbCXA5BGteP3x0A0hTnRCl+O3Shbzo9H+89GW2Och9g6E4ciNorX8YNj43akn8QlJpAgAgL5rRZTd3br5IQrwE4eQxIrg7B8soH+CVfmBCgF+Oikk8SI4vZcKyucfJgJEOiUjNSI4ey7kPedXtohaSOpEAADsPJsPef+3JiohqRTBiUpIFDJSLYKTBCGZEAHgfrnKjAiOq0IyJ4LjWrnKrAiOK0IyLwLAjXJFIgTiXB0kQkIcQkhEAJ0uVySiBZ0SQiI0ibpckQhDohJCIkIQhQwS0QY2hZAIC9gQQiIs0o4QEmGZsDJIRESYCvkPVqSEQk8gl0YAAAAASUVORK5CYII="/>
|
||||
<image id="_Image16" width="81px" height="48px" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFEAAAAwCAYAAABpJ5bJAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAC4klEQVRoge3aO2gUQRgH8P/MWcTHeRpjIXaKkEax0ULBQmyCIDZaiCD4ACWNCilE0UKCEAIiAUVEFJs0qRQstFAQxCaFWAhaiI0IyZmXSezGIvfdzczu3c3O7t4+Zv7NLewuzPfLNzs7S9ieCzMCBvn+dDszuc7FMFNEiscMJjIixWO2wm1vtMUvY6w7UY7rXZkIIsVVTAYkPzVdw1SK9V1pl9BCPWa0tC3QT3HzdC3Md2X3GBflMdsncjEeMxirIjykmlgFeMy1JDJw1zETG7DLkIkPNg4m50CFNX45w+eHA4XATG2QUTB1PN48Xvv9dD/fmKkOrhMkYwBnBMWk4xaojPp+bFtuIXsyMBkzKh6do+PXd/OH2dMBDV6aETLeugrUqcsaeBwqLgc4Y8p1U7f6c4PZ84HsH54VBGGDp3Ymw4uRrZljZjaAQ9dnRRw8efo/uZotZOZ/xWM36lpnas/CJhZDeAc3Hg8ceHB5Syb1ZI4IAMdv10UcvIp27t753mLmApFyevSPaL0fdli5taku30PX3Tlb61ltuUKknBufE7Z4emeOnEofM5eIlCsTc0LHU3HbLT4s0MHDJzanVmuuEQHg2uN5EZzW5njyPReHqqnUm3tEys1n88IWT9/1nDmaLGZhECmjkwtCXp07rtyhC1PrnpOHNyVSf+EQAWB8arHNrscMT+7MoYPxIQuJSHn0alE0gTRQeecTfPcMfnI7sm+jtUWhESnP3yyFdKYZnv5sPTC4IbJJKRABYPIdQQbxlOenwbvn3l3RIEuDSHn58a9QUczx9C9Ku3euN/IpHSLl7fRytF2P/EVJ2/XsGOiMWVpEAPjwZVkEgNqt3Mo2Ur2HztWqfaFepUakTH9bCdn1mOPJCYN0ApHy9edK8JObgtuazryLjIzpFCIA/Pi1KuLg6alV+5hziJTf9VURB0+Os4iUhaV/sf/9xXlEShxMjyjFFtIjhiQqpkfsEFPM/22HvcYsagRUAAAAAElFTkSuQmCC"/>
|
||||
<image id="_Image19" width="13px" height="118px" xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAB2AA0DAREAAhEBAxEB/8QAGAABAQEBAQAAAAAAAAAAAAAAAwQAAgb/xAAbEAACAwEBAQAAAAAAAAAAAAAAAwECYSExEf/EABkBAAMBAQEAAAAAAAAAAAAAAAECAwAEB//EABgRAQADAQAAAAAAAAAAAAAAAAABERIC/9oADAMBAAIRAxEAPwDxh7688YzOopMgtqdQqQaGltUYQntSILCOeC7Gl1EYQntSiwjngk9jldRGEJ7UotUc8FnsaXVRhzz2pktUc8F2altEYQns9FhHPBdmpdVGHPPamS1RwXY5XURhzz2pRYRzwXY0uojCE9qZLCeeCbHK6qMIT2pRYRzwXZsrqow557PkkIwXRqX0RhCe1KJCOeCT2NL6pwhPakQSEx8F0NGEMxmf/9k="/>
|
||||
<radialGradient id="_Radial22" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(139.128,29.5437,-60.595,223.638,1611.84,2160.46)"><stop offset="0" style="stop-color:rgb(224,235,247);stop-opacity:1"/><stop offset="0.57" style="stop-color:rgb(203,221,240);stop-opacity:1"/><stop offset="0.79" style="stop-color:rgb(159,193,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,166,214);stop-opacity:1"/></radialGradient>
|
||||
<radialGradient id="_Radial23" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(139.128,29.5437,-60.595,223.638,1611.84,2160.46)"><stop offset="0" style="stop-color:rgb(224,235,247);stop-opacity:1"/><stop offset="0.57" style="stop-color:rgb(203,221,240);stop-opacity:1"/><stop offset="0.79" style="stop-color:rgb(159,193,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,166,214);stop-opacity:1"/></radialGradient>
|
||||
<radialGradient id="_Radial24" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(139.128,29.5437,-60.595,223.638,1611.84,2160.46)"><stop offset="0" style="stop-color:rgb(224,235,247);stop-opacity:1"/><stop offset="0.57" style="stop-color:rgb(203,221,240);stop-opacity:1"/><stop offset="0.79" style="stop-color:rgb(159,193,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,166,214);stop-opacity:1"/></radialGradient>
|
||||
<radialGradient id="_Radial25" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(139.128,29.5437,-60.595,223.638,1611.84,2160.46)"><stop offset="0" style="stop-color:rgb(224,235,247);stop-opacity:1"/><stop offset="0.57" style="stop-color:rgb(203,221,240);stop-opacity:1"/><stop offset="0.79" style="stop-color:rgb(159,193,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,166,214);stop-opacity:1"/></radialGradient>
|
||||
<radialGradient id="_Radial26" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(139.128,29.5437,-60.595,223.638,1611.84,2160.46)"><stop offset="0" style="stop-color:rgb(224,235,247);stop-opacity:1"/><stop offset="0.57" style="stop-color:rgb(203,221,240);stop-opacity:1"/><stop offset="0.79" style="stop-color:rgb(159,193,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,166,214);stop-opacity:1"/></radialGradient>
|
||||
<radialGradient id="_Radial27" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(139.128,29.5437,-60.595,223.638,1611.84,2160.46)"><stop offset="0" style="stop-color:rgb(224,235,247);stop-opacity:1"/><stop offset="0.57" style="stop-color:rgb(203,221,240);stop-opacity:1"/><stop offset="0.79" style="stop-color:rgb(159,193,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,166,214);stop-opacity:1"/></radialGradient>
|
||||
<radialGradient id="_Radial28" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(139.128,29.5437,-60.595,223.638,1611.84,2160.46)"><stop offset="0" style="stop-color:rgb(224,235,247);stop-opacity:1"/><stop offset="0.57" style="stop-color:rgb(203,221,240);stop-opacity:1"/><stop offset="0.79" style="stop-color:rgb(159,193,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(118,166,214);stop-opacity:1"/></radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 3.5 MiB |
@@ -1,40 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import VTooltip from 'v-tooltip'
|
||||
import VueMixpanel from 'vue-mixpanel'
|
||||
import PortalVue from 'portal-vue'
|
||||
import { formatNumber } from '@/plugins/formatNumber'
|
||||
|
||||
/**
|
||||
* Global bootstrapping for the frontend app
|
||||
*/
|
||||
|
||||
// Filter to turn any number into a nice string like '10k', '5.5m'
|
||||
// Accepts 'max' parameter to set it's formatting while being animated
|
||||
Vue.filter('prettynum', formatNumber)
|
||||
|
||||
// env vars injected by Vite
|
||||
const enableDevMode = !!import.meta.env.FORCE_VUE_DEVTOOLS || !!import.meta.env.DEV
|
||||
|
||||
Vue.config.productionTip = enableDevMode
|
||||
Vue.config.devtools = enableDevMode
|
||||
|
||||
Vue.use(VTooltip, {
|
||||
defaultDelay: 300,
|
||||
defaultBoundariesElement: document.body,
|
||||
defaultHtml: false
|
||||
})
|
||||
|
||||
// In highly restrictive sandboxed environments mixpanel init might fail due to document.cookie access
|
||||
Vue.use(VueMixpanel, {
|
||||
token: 'acd87c5a50b56df91a795e999812a3a4',
|
||||
config: {
|
||||
// eslint-disable-next-line camelcase
|
||||
api_host: 'https://analytics.speckle.systems'
|
||||
}
|
||||
})
|
||||
|
||||
Vue.use(PortalVue)
|
||||
|
||||
// Event hub
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
Vue.prototype.$eventHub = new Vue()
|
||||
@@ -1,300 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import Vue from 'vue'
|
||||
import { createApolloProvider, ApolloProvider } from '@vue/apollo-option'
|
||||
import {
|
||||
ApolloClient,
|
||||
ApolloLink,
|
||||
InMemoryCache,
|
||||
split,
|
||||
TypePolicies,
|
||||
from
|
||||
} 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'
|
||||
import { merge } from 'lodash'
|
||||
import { statePolicies as commitObjectViewerStatePolicies } from '@/main/lib/viewer/commit-object-viewer/stateManagerCore'
|
||||
import { Optional } from '@speckle/shared'
|
||||
import { onError } from '@apollo/client/link/error'
|
||||
import { registerError, isErrorState } from '@/main/lib/core/utils/appErrorStateManager'
|
||||
import { isInvalidAuth } from '@/helpers/errorHelper'
|
||||
import { signOut } from '@/plugins/authHelpers'
|
||||
|
||||
// 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 = (import.meta.env.SPECKLE_SERVER_VERSION || 'unknown') as string
|
||||
|
||||
let instance: Optional<ApolloProvider> = undefined
|
||||
|
||||
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: merge<TypePolicies, TypePolicies>(
|
||||
{
|
||||
Query: {
|
||||
fields: {
|
||||
otherUser: {
|
||||
read(original, { args, toReference }) {
|
||||
if (args?.id) {
|
||||
return toReference({ __typename: 'LimitedUser', id: args.id })
|
||||
}
|
||||
|
||||
return original
|
||||
}
|
||||
},
|
||||
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: ['query'],
|
||||
merge: buildAbstractCollectionMergeFunction('StreamCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
LimitedUser: {
|
||||
fields: {
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
User: {
|
||||
fields: {
|
||||
timeline: {
|
||||
keyArgs: ['after', 'before'],
|
||||
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
|
||||
},
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
},
|
||||
favoriteStreams: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('StreamCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
Stream: {
|
||||
fields: {
|
||||
activity: {
|
||||
keyArgs: ['after', 'before', 'actionType'],
|
||||
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
|
||||
},
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
},
|
||||
pendingCollaborators: {
|
||||
merge: incomingOverwritesExistingMergeFunction
|
||||
},
|
||||
pendingAccessRequests: {
|
||||
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
|
||||
}
|
||||
},
|
||||
commitObjectViewerStatePolicies
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// WS link
|
||||
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
|
||||
)
|
||||
|
||||
// Stopping WS when in error state
|
||||
wsClient.use([
|
||||
{
|
||||
applyMiddleware: (_opt, next) => {
|
||||
if (isErrorState()) {
|
||||
return // never invokes next() - essentially stuck
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
// Global error handling
|
||||
const errorLink = onError((res) => {
|
||||
const { networkError } = res
|
||||
if (networkError && isInvalidAuth(networkError)) {
|
||||
// Logout
|
||||
void signOut()
|
||||
}
|
||||
|
||||
registerError()
|
||||
})
|
||||
|
||||
return from([errorLink, link])
|
||||
}
|
||||
|
||||
function createApolloClient() {
|
||||
const cache = createCache()
|
||||
const wsClient = createWsClient()
|
||||
const link = createLink(wsClient)
|
||||
|
||||
const apolloClient = new ApolloClient({
|
||||
link,
|
||||
cache,
|
||||
ssrForceFetchDelay: 100,
|
||||
connectToDevTools: import.meta.env.DEV,
|
||||
name: 'web',
|
||||
version: appVersion
|
||||
})
|
||||
|
||||
return {
|
||||
apolloClient,
|
||||
wsClient
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and set a global 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
|
||||
})
|
||||
instance = apolloProvider
|
||||
|
||||
return apolloProvider
|
||||
}
|
||||
|
||||
export function getApolloProvider(): ApolloProvider {
|
||||
if (!instance) {
|
||||
throw new Error('Attempting to use unitialized global Apollo Provider')
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
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,32 +0,0 @@
|
||||
import { basicStreamAccessRequestFieldsFragment } from '@/graphql/fragments/accessRequests'
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export const getStreamAccessRequestQuery = gql`
|
||||
query GetStreamAccessRequest($streamId: String!) {
|
||||
streamAccessRequest(streamId: $streamId) {
|
||||
...BasicStreamAccessRequestFields
|
||||
}
|
||||
}
|
||||
|
||||
${basicStreamAccessRequestFieldsFragment}
|
||||
`
|
||||
|
||||
export const createStreamAccessRequestMutation = gql`
|
||||
mutation CreateStreamAccessRequest($streamId: String!) {
|
||||
streamAccessRequestCreate(streamId: $streamId) {
|
||||
...BasicStreamAccessRequestFields
|
||||
}
|
||||
}
|
||||
|
||||
${basicStreamAccessRequestFieldsFragment}
|
||||
`
|
||||
|
||||
export const useStreamAccessRequestMutation = gql`
|
||||
mutation UseStreamAccessRequest(
|
||||
$requestId: String!
|
||||
$accept: Boolean!
|
||||
$role: StreamRole = STREAM_CONTRIBUTOR
|
||||
) {
|
||||
streamAccessRequestUse(requestId: $requestId, accept: $accept, role: $role)
|
||||
}
|
||||
`
|
||||
@@ -1,27 +0,0 @@
|
||||
query StreamWithBranch($streamId: String!, $branchName: String!, $cursor: String) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
name
|
||||
role
|
||||
branch(name: $branchName) {
|
||||
id
|
||||
name
|
||||
description
|
||||
commits(cursor: $cursor, limit: 4) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
authorName
|
||||
authorId
|
||||
authorAvatar
|
||||
sourceApplication
|
||||
message
|
||||
referencedObject
|
||||
createdAt
|
||||
commentCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export const branchCreatedSubscription = gql`
|
||||
subscription BranchCreated($streamId: String!) {
|
||||
branchCreated(streamId: $streamId)
|
||||
}
|
||||
`
|
||||
// TODO: Reusable composable
|
||||
export const streamNavBranchesQuery = gql`
|
||||
query StreamAllBranches($streamId: String!, $cursor: String) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
branches(limit: 500, cursor: $cursor) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
name
|
||||
description
|
||||
author {
|
||||
id
|
||||
name
|
||||
}
|
||||
commits {
|
||||
totalCount
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -1,31 +0,0 @@
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export const COMMENT_FULL_INFO_FRAGMENT = gql`
|
||||
fragment CommentFullInfo on Comment {
|
||||
id
|
||||
archived
|
||||
authorId
|
||||
text {
|
||||
doc
|
||||
attachments {
|
||||
id
|
||||
fileName
|
||||
streamId
|
||||
fileType
|
||||
fileSize
|
||||
}
|
||||
}
|
||||
data
|
||||
screenshot
|
||||
replies {
|
||||
totalCount
|
||||
}
|
||||
resources {
|
||||
resourceId
|
||||
resourceType
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
viewedAt
|
||||
}
|
||||
`
|
||||
@@ -1,18 +0,0 @@
|
||||
query StreamCommitQuery($streamId: String!, $id: String!) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
name
|
||||
role
|
||||
commit(id: $id) {
|
||||
id
|
||||
message
|
||||
referencedObject
|
||||
authorName
|
||||
authorId
|
||||
authorAvatar
|
||||
createdAt
|
||||
branchName
|
||||
sourceApplication
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export const streamBranchesSelectorQuery = gql`
|
||||
query StreamBranchesSelector($streamId: String!) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
branches(limit: 100) {
|
||||
items {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const moveCommitsMutation = gql`
|
||||
mutation MoveCommits($input: CommitsMoveInput!) {
|
||||
commitsMove(input: $input)
|
||||
}
|
||||
`
|
||||
|
||||
export const deleteCommitsMutation = gql`
|
||||
mutation DeleteCommits($input: CommitsDeleteInput!) {
|
||||
commitsDelete(input: $input)
|
||||
}
|
||||
`
|
||||
@@ -1,22 +0,0 @@
|
||||
import { limitedUserFieldsFragment } from '@/graphql/fragments/user'
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export const basicStreamAccessRequestFieldsFragment = gql`
|
||||
fragment BasicStreamAccessRequestFields on StreamAccessRequest {
|
||||
id
|
||||
streamId
|
||||
createdAt
|
||||
}
|
||||
`
|
||||
|
||||
export const fullStreamAccessRequestFieldsFragment = gql`
|
||||
fragment FullStreamAccessRequestFields on StreamAccessRequest {
|
||||
...BasicStreamAccessRequestFields
|
||||
requester {
|
||||
...LimitedUserFields
|
||||
}
|
||||
}
|
||||
|
||||
${limitedUserFieldsFragment}
|
||||
${basicStreamAccessRequestFieldsFragment}
|
||||
`
|
||||
@@ -1,25 +0,0 @@
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export const activityMainFieldsFragment = gql`
|
||||
fragment ActivityMainFields on Activity {
|
||||
id
|
||||
actionType
|
||||
info
|
||||
userId
|
||||
streamId
|
||||
resourceId
|
||||
resourceType
|
||||
time
|
||||
message
|
||||
}
|
||||
`
|
||||
|
||||
export const limitedCommitActivityFieldsFragment = gql`
|
||||
fragment LimitedCommitActivityFields on Activity {
|
||||
id
|
||||
info
|
||||
time
|
||||
userId
|
||||
message
|
||||
}
|
||||
`
|
||||
@@ -1,27 +0,0 @@
|
||||
import { fullStreamAccessRequestFieldsFragment } from '@/graphql/fragments/accessRequests'
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export const streamPendingAccessRequestsFragment = gql`
|
||||
fragment StreamPendingAccessRequests on Stream {
|
||||
pendingAccessRequests {
|
||||
...FullStreamAccessRequestFields
|
||||
}
|
||||
}
|
||||
|
||||
${fullStreamAccessRequestFieldsFragment}
|
||||
`
|
||||
|
||||
export const streamFileUploadFragment = gql`
|
||||
fragment StreamFileUpload on FileUpload {
|
||||
id
|
||||
convertedCommitId
|
||||
userId
|
||||
convertedStatus
|
||||
convertedMessage
|
||||
fileName
|
||||
fileType
|
||||
uploadComplete
|
||||
uploadDate
|
||||
convertedLastUpdate
|
||||
}
|
||||
`
|
||||
@@ -1,38 +0,0 @@
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export const limitedUserFieldsFragment = gql`
|
||||
fragment LimitedUserFields on LimitedUser {
|
||||
id
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
}
|
||||
`
|
||||
|
||||
export const streamCollaboratorFieldsFragment = gql`
|
||||
fragment StreamCollaboratorFields on StreamCollaborator {
|
||||
id
|
||||
name
|
||||
role
|
||||
company
|
||||
avatar
|
||||
serverRole
|
||||
}
|
||||
`
|
||||
|
||||
export const usersOwnInviteFieldsFragment = gql`
|
||||
fragment UsersOwnInviteFields on PendingStreamCollaborator {
|
||||
id
|
||||
inviteId
|
||||
streamId
|
||||
streamName
|
||||
token
|
||||
invitedBy {
|
||||
...LimitedUserFields
|
||||
}
|
||||
}
|
||||
|
||||
${limitedUserFieldsFragment}
|
||||
`
|
||||
@@ -1,58 +0,0 @@
|
||||
import { gql } from '@apollo/client/core'
|
||||
import { usersOwnInviteFieldsFragment } from '@/graphql/fragments/user'
|
||||
|
||||
export const streamInviteQuery = gql`
|
||||
query StreamInvite($streamId: String!, $token: String) {
|
||||
streamInvite(streamId: $streamId, token: $token) {
|
||||
...UsersOwnInviteFields
|
||||
}
|
||||
}
|
||||
|
||||
${usersOwnInviteFieldsFragment}
|
||||
`
|
||||
|
||||
export const userStreamInvitesQuery = gql`
|
||||
query UserStreamInvites {
|
||||
streamInvites {
|
||||
...UsersOwnInviteFields
|
||||
}
|
||||
}
|
||||
|
||||
${usersOwnInviteFieldsFragment}
|
||||
`
|
||||
|
||||
export const useStreamInviteMutation = gql`
|
||||
mutation UseStreamInvite($accept: Boolean!, $streamId: String!, $token: String!) {
|
||||
streamInviteUse(accept: $accept, streamId: $streamId, token: $token)
|
||||
}
|
||||
`
|
||||
|
||||
export const cancelStreamInviteMutation = gql`
|
||||
mutation CancelStreamInvite($streamId: String!, $inviteId: String!) {
|
||||
streamInviteCancel(streamId: $streamId, inviteId: $inviteId)
|
||||
}
|
||||
`
|
||||
|
||||
export const deleteInviteMutation = gql`
|
||||
mutation DeleteInvite($inviteId: String!) {
|
||||
inviteDelete(inviteId: $inviteId)
|
||||
}
|
||||
`
|
||||
|
||||
export const resendInviteMutation = gql`
|
||||
mutation ResendInvite($inviteId: String!) {
|
||||
inviteResend(inviteId: $inviteId)
|
||||
}
|
||||
`
|
||||
|
||||
export const batchInviteToServerMutation = gql`
|
||||
mutation BatchInviteToServer($paramsArray: [ServerInviteCreateInput!]!) {
|
||||
serverInviteBatchCreate(input: $paramsArray)
|
||||
}
|
||||
`
|
||||
|
||||
export const batchInviteToStreamsMutation = gql`
|
||||
mutation BatchInviteToStreams($paramsArray: [StreamInviteCreateInput!]!) {
|
||||
streamInviteBatchCreate(input: $paramsArray)
|
||||
}
|
||||
`
|
||||
@@ -1,25 +0,0 @@
|
||||
extend type Query {
|
||||
"""
|
||||
Commit/Object viewer state (local-only)
|
||||
"""
|
||||
commitObjectViewerState: CommitObjectViewerState!
|
||||
}
|
||||
|
||||
type CommitObjectViewerState {
|
||||
viewerBusy: Boolean!
|
||||
selectedCommentMetaData: SelectedCommentMetaData
|
||||
addingComment: Boolean!
|
||||
preventCommentCollapse: Boolean!
|
||||
commentReactions: [String!]!
|
||||
emojis: [String!]!
|
||||
currentFilterState: JSONObject
|
||||
selectedObjects: [JSONObject]
|
||||
objectProperties: [JSONObject]
|
||||
localFilterPropKey: String
|
||||
sectionBox: Boolean
|
||||
}
|
||||
|
||||
type SelectedCommentMetaData {
|
||||
id: String!
|
||||
selectionLocation: JSONObject!
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
query StreamObject($streamId: String!, $id: String!) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
object(id: $id) {
|
||||
totalChildrenCount
|
||||
id
|
||||
speckleType
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
query StreamObjectNoData($streamId: String!, $id: String!) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
name
|
||||
object(id: $id) {
|
||||
totalChildrenCount
|
||||
id
|
||||
speckleType
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export const serverInfoBlobSizeFragment = gql`
|
||||
fragment ServerInfoBlobSizeFields on ServerInfo {
|
||||
configuration {
|
||||
blobSizeLimitBytes
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const mainServerInfoFieldsFragment = gql`
|
||||
fragment MainServerInfoFields on ServerInfo {
|
||||
name
|
||||
company
|
||||
description
|
||||
adminContact
|
||||
canonicalUrl
|
||||
termsOfService
|
||||
inviteOnly
|
||||
version
|
||||
guestModeEnabled
|
||||
enableNewWebUiMessaging
|
||||
migration {
|
||||
movedTo
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const serverInfoRolesFieldsFragment = gql`
|
||||
fragment ServerInfoRolesFields on ServerInfo {
|
||||
serverRoles {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const serverInfoScopesFieldsFragment = gql`
|
||||
fragment ServerInfoScopesFields on ServerInfo {
|
||||
scopes {
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Get main server info
|
||||
*/
|
||||
export const mainServerInfoQuery = gql`
|
||||
query MainServerInfo {
|
||||
serverInfo {
|
||||
...MainServerInfoFields
|
||||
}
|
||||
}
|
||||
|
||||
${mainServerInfoFieldsFragment}
|
||||
`
|
||||
|
||||
export const fullServerInfoQuery = gql`
|
||||
query FullServerInfo {
|
||||
serverInfo {
|
||||
...MainServerInfoFields
|
||||
...ServerInfoRolesFields
|
||||
...ServerInfoScopesFields
|
||||
...ServerInfoBlobSizeFields
|
||||
}
|
||||
}
|
||||
|
||||
${mainServerInfoFieldsFragment}
|
||||
${serverInfoRolesFieldsFragment}
|
||||
${serverInfoScopesFieldsFragment}
|
||||
${serverInfoBlobSizeFragment}
|
||||
`
|
||||
|
||||
export const serverInfoBlobSizeLimitQuery = gql`
|
||||
query ServerInfoBlobSizeLimit {
|
||||
serverInfo {
|
||||
...ServerInfoBlobSizeFields
|
||||
}
|
||||
}
|
||||
${serverInfoBlobSizeFragment}
|
||||
`
|
||||
|
||||
export const availableServerRolesQuery = gql`
|
||||
query AvailableServerRoles {
|
||||
serverInfo {
|
||||
serverRoles {
|
||||
id
|
||||
title
|
||||
}
|
||||
guestModeEnabled
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -1,20 +0,0 @@
|
||||
query StreamCommits($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
role
|
||||
commits {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
authorId
|
||||
authorName
|
||||
authorAvatar
|
||||
createdAt
|
||||
message
|
||||
referencedObject
|
||||
branchName
|
||||
sourceApplication
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
query Streams($cursor: String) {
|
||||
streams(cursor: $cursor, limit: 10) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
name
|
||||
description
|
||||
role
|
||||
isPublic
|
||||
createdAt
|
||||
updatedAt
|
||||
commentCount
|
||||
collaborators {
|
||||
id
|
||||
name
|
||||
company
|
||||
avatar
|
||||
role
|
||||
}
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
createdAt
|
||||
message
|
||||
authorId
|
||||
branchName
|
||||
authorName
|
||||
authorAvatar
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
branches {
|
||||
totalCount
|
||||
}
|
||||
favoritedDate
|
||||
favoritesCount
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
import { fullStreamAccessRequestFieldsFragment } from '@/graphql/fragments/accessRequests'
|
||||
import { activityMainFieldsFragment } from '@/graphql/fragments/activity'
|
||||
import {
|
||||
limitedUserFieldsFragment,
|
||||
streamCollaboratorFieldsFragment
|
||||
} from '@/graphql/fragments/user'
|
||||
import { gql } from '@apollo/client/core'
|
||||
import { streamFileUploadFragment } from '@/graphql/fragments/streams'
|
||||
|
||||
/**
|
||||
* Common stream fields when querying for streams
|
||||
*/
|
||||
export const commonStreamFieldsFragment = gql`
|
||||
fragment CommonStreamFields on Stream {
|
||||
id
|
||||
name
|
||||
description
|
||||
role
|
||||
isPublic
|
||||
createdAt
|
||||
updatedAt
|
||||
commentCount
|
||||
collaborators {
|
||||
...StreamCollaboratorFields
|
||||
}
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
}
|
||||
branches {
|
||||
totalCount
|
||||
}
|
||||
favoritedDate
|
||||
favoritesCount
|
||||
}
|
||||
|
||||
${streamCollaboratorFieldsFragment}
|
||||
`
|
||||
|
||||
/**
|
||||
* Retrieve a single stream
|
||||
*/
|
||||
export const streamQuery = gql`
|
||||
query Stream($id: String!) {
|
||||
stream(id: $id) {
|
||||
...CommonStreamFields
|
||||
}
|
||||
}
|
||||
|
||||
${commonStreamFieldsFragment}
|
||||
`
|
||||
|
||||
/**
|
||||
* Retrieve stream collaborators info
|
||||
*/
|
||||
export const streamWithCollaboratorsQuery = gql`
|
||||
query StreamWithCollaborators($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
name
|
||||
isPublic
|
||||
role
|
||||
collaborators {
|
||||
...StreamCollaboratorFields
|
||||
}
|
||||
pendingCollaborators {
|
||||
title
|
||||
inviteId
|
||||
role
|
||||
user {
|
||||
...LimitedUserFields
|
||||
}
|
||||
}
|
||||
pendingAccessRequests {
|
||||
...FullStreamAccessRequestFields
|
||||
}
|
||||
}
|
||||
}
|
||||
${limitedUserFieldsFragment}
|
||||
${streamCollaboratorFieldsFragment}
|
||||
${fullStreamAccessRequestFieldsFragment}
|
||||
`
|
||||
|
||||
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 {
|
||||
...ActivityMainFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${activityMainFieldsFragment}
|
||||
`
|
||||
|
||||
/**
|
||||
* Remove authenticated user from the collaborators list
|
||||
*/
|
||||
export const leaveStreamMutation = gql`
|
||||
mutation LeaveStream($streamId: String!) {
|
||||
streamLeave(streamId: $streamId)
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Update a user's stream permission
|
||||
*/
|
||||
export const updateStreamPermissionMutation = gql`
|
||||
mutation UpdateStreamPermission($params: StreamUpdatePermissionInput!) {
|
||||
streamUpdatePermission(permissionParams: $params)
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Get a stream's first commit
|
||||
*/
|
||||
export const streamFirstCommitQuery = gql`
|
||||
query StreamFirstCommit($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Get a stream branch's first commit
|
||||
*/
|
||||
export const streamBranchFirstCommitQuery = gql`
|
||||
query StreamBranchFirstCommit($id: String!, $branch: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
branch(name: $branch) {
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const streamSettingsQuery = gql`
|
||||
query StreamSettings($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
name
|
||||
description
|
||||
isPublic
|
||||
isDiscoverable
|
||||
allowPublicComments
|
||||
role
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const searchStreamsQuery = gql`
|
||||
query SearchStreams($query: String) {
|
||||
streams(query: $query) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
name
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const updateStreamSettingsMutation = gql`
|
||||
mutation UpdateStreamSettings($input: StreamUpdateInput!) {
|
||||
streamUpdate(stream: $input)
|
||||
}
|
||||
`
|
||||
|
||||
export const deleteStreamMutation = gql`
|
||||
mutation DeleteStream($id: String!) {
|
||||
streamDelete(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
export const shareableStreamQuery = gql`
|
||||
query ShareableStream($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
isPublic
|
||||
role
|
||||
collaborators {
|
||||
...StreamCollaboratorFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${streamCollaboratorFieldsFragment}
|
||||
`
|
||||
|
||||
export const streamFileUploadsUpdatedSubscription = gql`
|
||||
subscription StreamFileUploadsUpdated($id: String!) {
|
||||
projectFileImportUpdated(id: $id) {
|
||||
type
|
||||
id
|
||||
upload {
|
||||
...StreamFileUpload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${streamFileUploadFragment}
|
||||
`
|
||||
@@ -1,193 +0,0 @@
|
||||
import { activityMainFieldsFragment } from '@/graphql/fragments/activity'
|
||||
import { limitedUserFieldsFragment } from '@/graphql/fragments/user'
|
||||
import { commonStreamFieldsFragment } from '@/graphql/streams'
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export const commonUserFieldsFragment = gql`
|
||||
fragment CommonUserFields on User {
|
||||
id
|
||||
email
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
hasPendingVerification
|
||||
profiles
|
||||
role
|
||||
streams {
|
||||
totalCount
|
||||
}
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* User data with favorite streams
|
||||
*/
|
||||
export const userFavoriteStreamsQuery = gql`
|
||||
query UserFavoriteStreams($cursor: String) {
|
||||
activeUser {
|
||||
...CommonUserFields
|
||||
favoriteStreams(cursor: $cursor, limit: 10) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
...CommonStreamFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${commonUserFieldsFragment}
|
||||
${commonStreamFieldsFragment}
|
||||
`
|
||||
|
||||
/**
|
||||
* Get main user metadata
|
||||
*/
|
||||
export const mainUserDataQuery = gql`
|
||||
query MainUserData {
|
||||
activeUser {
|
||||
...CommonUserFields
|
||||
}
|
||||
}
|
||||
|
||||
${commonUserFieldsFragment}
|
||||
`
|
||||
|
||||
/**
|
||||
* Main metadata + extra info shown on profile page
|
||||
*/
|
||||
export const profileSelfQuery = gql`
|
||||
query ProfileSelf {
|
||||
activeUser {
|
||||
...CommonUserFields
|
||||
totalOwnedStreamsFavorites
|
||||
notificationPreferences
|
||||
}
|
||||
}
|
||||
|
||||
${commonUserFieldsFragment}
|
||||
`
|
||||
|
||||
/**
|
||||
* (Limited, not admin) User search
|
||||
*/
|
||||
export const userSearchQuery = gql`
|
||||
query UserSearch($query: String!, $limit: Int!, $cursor: String, $archived: Boolean) {
|
||||
userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {
|
||||
cursor
|
||||
items {
|
||||
...LimitedUserFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${limitedUserFieldsFragment}
|
||||
`
|
||||
|
||||
/**
|
||||
* Basic query for checking if user is logged in
|
||||
*/
|
||||
export const isLoggedInQuery = gql`
|
||||
query IsLoggedIn {
|
||||
activeUser {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Admin panel (invited/registered) users list
|
||||
*/
|
||||
export const adminUsersListQuery = gql`
|
||||
query AdminUsersList($limit: Int, $offset: Int, $query: String) {
|
||||
adminUsers(limit: $limit, offset: $offset, query: $query) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
registeredUser {
|
||||
id
|
||||
email
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
profiles
|
||||
role
|
||||
authorizedApps {
|
||||
name
|
||||
}
|
||||
}
|
||||
invitedUser {
|
||||
id
|
||||
email
|
||||
invitedBy {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const userTimelineQuery = gql`
|
||||
query UserTimeline($cursor: DateTime) {
|
||||
activeUser {
|
||||
id
|
||||
timeline(cursor: $cursor) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
...ActivityMainFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${activityMainFieldsFragment}
|
||||
`
|
||||
|
||||
export const validatePasswordStrengthQuery = gql`
|
||||
query ValidatePasswordStrength($pwd: String!) {
|
||||
userPwdStrength(pwd: $pwd) {
|
||||
score
|
||||
feedback {
|
||||
warning
|
||||
suggestions
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const emailVerificationBannerStateQuery = gql`
|
||||
query EmailVerificationBannerState {
|
||||
activeUser {
|
||||
id
|
||||
email
|
||||
verified
|
||||
hasPendingVerification
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const requestVerificationMutation = gql`
|
||||
mutation RequestVerification {
|
||||
requestVerification
|
||||
}
|
||||
`
|
||||
|
||||
export const updateUserNotificationPreferencesMutation = gql`
|
||||
mutation UpdateUserNotificationPreferences($preferences: JSONObject!) {
|
||||
userNotificationPreferencesUpdate(preferences: $preferences)
|
||||
}
|
||||
`
|
||||
@@ -1,10 +0,0 @@
|
||||
query UserById($id: String!) {
|
||||
otherUser(id: $id) {
|
||||
id
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
query UserProfile($id: String!) {
|
||||
otherUser(id: $id) {
|
||||
id
|
||||
name
|
||||
bio
|
||||
company
|
||||
avatar
|
||||
verified
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
query webhook($streamId: String!, $webhookId: String!) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
role
|
||||
webhooks(id: $webhookId) {
|
||||
items {
|
||||
id
|
||||
streamId
|
||||
url
|
||||
description
|
||||
triggers
|
||||
enabled
|
||||
history(limit: 1) {
|
||||
items {
|
||||
status
|
||||
statusInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
query webhooks($streamId: String!) {
|
||||
stream(id: $streamId) {
|
||||
id
|
||||
name
|
||||
role
|
||||
webhooks {
|
||||
items {
|
||||
id
|
||||
streamId
|
||||
url
|
||||
description
|
||||
triggers
|
||||
enabled
|
||||
history(limit: 50) {
|
||||
items {
|
||||
status
|
||||
statusInfo
|
||||
lastUpdate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { ApolloError, ServerError, ServerParseError } from '@apollo/client/core'
|
||||
import { NetworkError } from '@apollo/client/errors'
|
||||
import { has, isString } from 'lodash'
|
||||
|
||||
/**
|
||||
* Base application error
|
||||
*/
|
||||
export abstract class BaseError extends Error {
|
||||
/**
|
||||
* Default message if none is passed
|
||||
*/
|
||||
static defaultMessage = 'Unexpected error occurred!'
|
||||
|
||||
constructor(message?: string, options?: ErrorOptions) {
|
||||
message ||= new.target.defaultMessage
|
||||
super(message, options)
|
||||
}
|
||||
}
|
||||
|
||||
const isServerError = (err: Error): err is ServerError =>
|
||||
has(err, 'response') && has(err, 'result') && has(err, 'statusCode')
|
||||
const isServerParseError = (err: Error): err is ServerParseError =>
|
||||
has(err, 'response') && has(err, 'bodyText') && has(err, 'statusCode')
|
||||
|
||||
export function isInvalidAuth(error: ApolloError | NetworkError) {
|
||||
const networkError = error instanceof ApolloError ? error.networkError : error
|
||||
if (
|
||||
!networkError ||
|
||||
(!isServerError(networkError) && !isServerParseError(networkError))
|
||||
)
|
||||
return false
|
||||
|
||||
const statusCode = networkError.statusCode
|
||||
const hasCorrectCode = [403].includes(statusCode)
|
||||
if (!hasCorrectCode) return false
|
||||
|
||||
const message: string | undefined = (
|
||||
isServerError(networkError)
|
||||
? isString(networkError.result)
|
||||
? networkError.result
|
||||
: networkError.result?.error
|
||||
: networkError.bodyText
|
||||
) as string | undefined
|
||||
|
||||
return (message || '').toLowerCase().includes('token')
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Roles } from '@speckle/shared'
|
||||
import type { ServerRoles, StreamRoles } from '@speckle/shared'
|
||||
import { StreamRole } from '@/graphql/generated/graphql'
|
||||
|
||||
/**
|
||||
* Keys for values stored in localStorage
|
||||
*/
|
||||
export const LocalStorageKeys = Object.freeze({
|
||||
AuthToken: 'AuthToken',
|
||||
RefreshToken: 'RefreshToken',
|
||||
Uuid: 'uuid',
|
||||
ShouldRedirectTo: 'shouldRedirectTo'
|
||||
})
|
||||
|
||||
/**
|
||||
* Our GQL schema has a StreamRoles enum that unfortunately can't have the same exact values as our roles constants, because
|
||||
* we can't use colons (:) there. So you can use this function to map from our constant value to the GQL one.
|
||||
*/
|
||||
export function streamRoleToGraphQLEnum(role: StreamRoles): StreamRole {
|
||||
switch (role) {
|
||||
case Roles.Stream.Owner:
|
||||
return StreamRole.StreamOwner
|
||||
case Roles.Stream.Reviewer:
|
||||
return StreamRole.StreamReviewer
|
||||
case Roles.Stream.Contributor:
|
||||
default:
|
||||
return StreamRole.StreamContributor
|
||||
}
|
||||
}
|
||||
|
||||
export { Roles, ServerRoles, StreamRoles }
|
||||
@@ -1,3 +0,0 @@
|
||||
import { md5 } from '@speckle/shared'
|
||||
export default md5
|
||||
export { md5 }
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Generate a random string of any length
|
||||
* @param {number} length
|
||||
* @returns
|
||||
*/
|
||||
export function randomString(length) {
|
||||
return Math.round(Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))
|
||||
.toString(36)
|
||||
.slice(1)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Check whether or not a stream can be favorited by the active user
|
||||
*/
|
||||
export function canBeFavorited(stream) {
|
||||
return stream && (stream.isPublic || stream.role)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
export { isUndefinedOrVoid } from '@speckle/shared'
|
||||
export type {
|
||||
Nullable,
|
||||
Optional,
|
||||
MaybeNullOrUndefined,
|
||||
MaybeAsync,
|
||||
MaybeFalsy
|
||||
} from '@speckle/shared'
|
||||
import { ReactiveVar } from '@apollo/client/core'
|
||||
import Vue, { VueConstructor } from 'vue'
|
||||
import { LooseRequired } from 'vue/types/common'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type GetReactiveVarType<V extends ReactiveVar<any>> = V extends ReactiveVar<
|
||||
infer T
|
||||
>
|
||||
? T
|
||||
: unknown
|
||||
|
||||
export type SetupProps<P = unknown> = Readonly<LooseRequired<P>>
|
||||
|
||||
// Copied from Vue typings & improved ergonomics
|
||||
export type CombinedVueInstance<
|
||||
Instance extends Vue = Vue,
|
||||
Data = unknown,
|
||||
Methods = unknown,
|
||||
Computed = unknown,
|
||||
Props = unknown
|
||||
> = Data & Methods & Computed & Props & Instance
|
||||
|
||||
export type ExtendedVue<
|
||||
Instance extends Vue = Vue,
|
||||
Data = unknown,
|
||||
Methods = unknown,
|
||||
Computed = unknown,
|
||||
Props = unknown
|
||||
> = VueConstructor<CombinedVueInstance<Instance, Data, Methods, Computed, Props> & Vue>
|
||||
|
||||
export type VueWithMixins<
|
||||
A extends VueConstructor = VueConstructor,
|
||||
B extends VueConstructor = VueConstructor,
|
||||
C extends VueConstructor = VueConstructor,
|
||||
D extends VueConstructor = VueConstructor,
|
||||
E extends VueConstructor = VueConstructor
|
||||
> = VueConstructor<
|
||||
Vue &
|
||||
InstanceType<A> &
|
||||
InstanceType<B> &
|
||||
InstanceType<C> &
|
||||
InstanceType<D> &
|
||||
InstanceType<E>
|
||||
>
|
||||
|
||||
/**
|
||||
* Create Vue base class with the specified mixins and correctly returned TypeScript types
|
||||
* @deprecated Use Composition API instead
|
||||
* @returns
|
||||
*/
|
||||
export function vueWithMixins<
|
||||
A extends VueConstructor = VueConstructor,
|
||||
B extends VueConstructor = VueConstructor,
|
||||
C extends VueConstructor = VueConstructor,
|
||||
D extends VueConstructor = VueConstructor,
|
||||
E extends VueConstructor = VueConstructor
|
||||
>(
|
||||
mixin1?: A,
|
||||
mixin2?: B,
|
||||
mixin3?: C,
|
||||
mixin4?: D,
|
||||
mixin5?: E
|
||||
): VueWithMixins<A, B, C, D, E> {
|
||||
const mixins = [mixin1, mixin2, mixin3, mixin4, mixin5].filter(
|
||||
(m): m is A | B | C | D | E => !!m
|
||||
)
|
||||
|
||||
return Vue.extend({
|
||||
mixins
|
||||
}) as VueWithMixins<A, B, C, D, E>
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { CombinedVueInstance } from 'vue/types/vue'
|
||||
|
||||
/**
|
||||
* Use this to type v-form $refs instances
|
||||
*/
|
||||
export type VFormInstance = CombinedVueInstance<
|
||||
Vue,
|
||||
unknown,
|
||||
{
|
||||
/**
|
||||
* Reset validation state
|
||||
*/
|
||||
resetValidation(): void
|
||||
/**
|
||||
* Validate the form and return whether it's valid or not
|
||||
*/
|
||||
validate(): boolean
|
||||
},
|
||||
unknown,
|
||||
unknown,
|
||||
unknown
|
||||
>
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="isAppErrorState && showBanner" class="app-error-state">
|
||||
<div class="app-error-state__wrapper">
|
||||
<div>
|
||||
Due to a large amount of errors some functionality has been disabled! Please
|
||||
reload the page or contact the server administrators.
|
||||
</div>
|
||||
<div>
|
||||
<v-btn v-tooltip="'Close banner'" icon @click="hideErrorStateBanner">
|
||||
<v-icon class="app-error-state__icon">mdi-close-circle</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { isErrorState } from '@/main/lib/core/utils/appErrorStateManager'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const showBanner = ref(true)
|
||||
const isAppErrorState = computed(() => isErrorState())
|
||||
const hideErrorStateBanner = () => (showBanner.value = false)
|
||||
</script>
|
||||
<style lang="css">
|
||||
.v-timeline:before {
|
||||
top: 40px !important;
|
||||
}
|
||||
|
||||
.app-error-state {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background-color: red;
|
||||
z-index: 1000;
|
||||
padding: 8px;
|
||||
font-family: 'Roboto', sans-serif !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.app-error-state__wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-error-state__icon {
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* Don't export anything out of this file and import it in other files, this borks Vite HMR for some reason
|
||||
* (runs app.js twice in the browser)!
|
||||
*/
|
||||
|
||||
import '@/bootstrapper'
|
||||
import Vue from 'vue'
|
||||
|
||||
import App from '@/main/App.vue'
|
||||
import { LocalStorageKeys } from '@/helpers/mainConstants'
|
||||
import * as MixpanelManager from '@/mixpanelManager'
|
||||
|
||||
import { provide } from 'vue'
|
||||
import { DefaultApolloClient } from '@vue/apollo-composable'
|
||||
import { createProvider, installVueApollo } from '@/config/apolloConfig'
|
||||
import {
|
||||
checkAccessCodeAndGetTokens,
|
||||
prefetchUserAndSetID
|
||||
} from '@/plugins/authHelpers'
|
||||
|
||||
import router from '@/main/router/index'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
import VueTimeago from 'vue-timeago'
|
||||
|
||||
Vue.use(VueTimeago, { locale: 'en' })
|
||||
|
||||
import VueFilterDateParse from '@vuejs-community/vue-filter-date-parse'
|
||||
Vue.use(VueFilterDateParse)
|
||||
|
||||
import VueFilterDateFormat from '@vuejs-community/vue-filter-date-format'
|
||||
Vue.use(VueFilterDateFormat)
|
||||
|
||||
// adds various helper methods
|
||||
import '@/plugins/helpers'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
import { InvalidAuthTokenError } from '@/main/lib/auth/errors'
|
||||
|
||||
// Async ApexChart load
|
||||
Vue.component('ApexChart', async () => {
|
||||
const VueApexCharts = await import('vue-apexcharts')
|
||||
Vue.use(VueApexCharts)
|
||||
|
||||
return VueApexCharts
|
||||
})
|
||||
|
||||
// Filter to capitalize words
|
||||
Vue.filter('capitalize', (value) => {
|
||||
if (!value) return ''
|
||||
value = value.toString()
|
||||
return value.charAt(0).toUpperCase() + value.slice(1)
|
||||
})
|
||||
|
||||
const apolloProvider = createProvider()
|
||||
installVueApollo(apolloProvider)
|
||||
|
||||
function postAuthInit() {
|
||||
// Init mixpanel
|
||||
MixpanelManager.initialize({
|
||||
hostApp: 'web',
|
||||
hostAppDisplayName: 'Web App'
|
||||
})
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
vuetify,
|
||||
setup() {
|
||||
provide(DefaultApolloClient, apolloProvider.defaultClient)
|
||||
},
|
||||
render: (h) => h(App)
|
||||
}).$mount('#app')
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const authToken = AppLocalStorage.get(LocalStorageKeys.AuthToken)
|
||||
|
||||
// no auth token - check if we can resolve it from access code
|
||||
if (!authToken) {
|
||||
const gotToken = await checkAccessCodeAndGetTokens()
|
||||
if (gotToken) {
|
||||
// Remove access_code get param from current url
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.delete('access_code')
|
||||
window.history.replaceState({}, document.title, url.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// try to retrieve user info with auth token
|
||||
try {
|
||||
await prefetchUserAndSetID(apolloProvider.defaultClient)
|
||||
} catch (e) {
|
||||
if (e instanceof InvalidAuthTokenError) {
|
||||
// data retrieval failed and user was logged out - go to login page
|
||||
window.location = `${window.location.origin}/authn/login`
|
||||
return
|
||||
}
|
||||
|
||||
// Log and continue
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
// Init app
|
||||
postAuthInit()
|
||||
}
|
||||
init()
|
||||
@@ -1,596 +0,0 @@
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<v-timeline-item v-show="shouldShowComponent" medium>
|
||||
<template #icon>
|
||||
<user-avatar v-if="user" :id="user.id" :avatar="user.avatar" :name="user.name" />
|
||||
</template>
|
||||
<v-row class="pt-1 timeline-activity">
|
||||
<v-col cols="12" class="mb-0 pb-0">
|
||||
<div v-if="user && you && stream" class="body-2">
|
||||
|
||||
<router-link :to="'/profile/' + user.id">
|
||||
{{ userName }}
|
||||
</router-link>
|
||||
<span> {{ lastActivityBrief.captionText }} </span>
|
||||
<span v-if="stream">
|
||||
<router-link :to="'/streams/' + stream.id">{{ stream.name }}</router-link>
|
||||
</span>
|
||||
<timeago :datetime="lastActivity.time" class="font-italic ma-1"></timeago>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<!-- STREAM PERMISSIONS -->
|
||||
<v-card
|
||||
v-if="lastActivity.actionType.includes('stream_permissions') && stream"
|
||||
class="activity-card"
|
||||
:flat="$vuetify.theme.dark"
|
||||
>
|
||||
<v-card-text class="pa-5 body-1">
|
||||
<v-container>
|
||||
<v-row
|
||||
v-for="activityItem in activityGroup"
|
||||
:key="activityItem.time"
|
||||
class="align-center"
|
||||
>
|
||||
<v-col cols="12" md="10">
|
||||
<user-pill
|
||||
class="mr-3"
|
||||
:user-id="activityItem.info.targetUser || activityItem.userId"
|
||||
:color="isUserAddedToStreamActivity ? 'success' : 'error'"
|
||||
></user-pill>
|
||||
|
||||
<span
|
||||
v-if="$vuetify.breakpoint.smAndUp"
|
||||
class="mr-3 body-2 font-italic"
|
||||
>
|
||||
{{ isUserAddedToStreamActivity ? 'user added as' : 'user removed' }}
|
||||
</span>
|
||||
<v-chip v-if="activityItem.info.role" small outlined class="my-2">
|
||||
<v-icon small left>mdi-account-key-outline</v-icon>
|
||||
{{ activityItem.info.role.split(':')[1] }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col v-if="$vuetify.breakpoint.mdAndUp" cols="2" class="text-right">
|
||||
<v-btn
|
||||
v-if="
|
||||
(activityItem.info.targetUser || activityItem.userId) &&
|
||||
isUserAddedToStreamActivity
|
||||
"
|
||||
text
|
||||
outlined
|
||||
small
|
||||
:to="
|
||||
'/profile/' +
|
||||
(activityItem.info.targetUser || activityItem.userId)
|
||||
"
|
||||
color="primary"
|
||||
>
|
||||
view
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- STREAM -->
|
||||
<v-card
|
||||
v-else-if="lastActivity.resourceType === 'stream' && stream"
|
||||
class="activity-card"
|
||||
:flat="$vuetify.theme.dark"
|
||||
>
|
||||
<v-card-text class="pa-5 body-1">
|
||||
<v-container>
|
||||
<v-row class="align-center">
|
||||
<router-link :to="url" class="title">
|
||||
<v-icon color="primary" small>mdi-folder</v-icon>
|
||||
{{ stream.name }}
|
||||
</router-link>
|
||||
<span class="ml-3 body-2 font-italic">
|
||||
{{ lastActivityBrief.actionText }}
|
||||
</span>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-btn
|
||||
v-if="
|
||||
(STREAM_CREATED_TYPES.includes(lastActivity.actionType) ||
|
||||
lastActivity.actionType === `stream_update`) &&
|
||||
$vuetify.breakpoint.mdAndUp
|
||||
"
|
||||
text
|
||||
outlined
|
||||
small
|
||||
exact
|
||||
:to="url"
|
||||
color="primary"
|
||||
>
|
||||
view
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<div class="mt-3">
|
||||
<list-item-activity-description
|
||||
v-for="(item, idx) in activityGroup"
|
||||
:key="item.time"
|
||||
:activity-group="activityGroup"
|
||||
:activity-item-index="idx"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pt-0">
|
||||
<div>
|
||||
<v-btn
|
||||
v-tooltip="
|
||||
stream.branches.totalCount +
|
||||
' branch' +
|
||||
(stream.branches.totalCount === 1 ? '' : 'es')
|
||||
"
|
||||
color="primary"
|
||||
text
|
||||
class="px-0 ml-3"
|
||||
small
|
||||
:to="'/streams/' + stream.id + '/branches'"
|
||||
>
|
||||
<v-icon small class="mr-2 float-left">mdi-source-branch</v-icon>
|
||||
{{ stream.branches.totalCount }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-tooltip="
|
||||
stream.commits.totalCount +
|
||||
' commit' +
|
||||
(stream.commits.totalCount === 1 ? '' : 's')
|
||||
"
|
||||
color="primary"
|
||||
text
|
||||
class="px-0 ml-3"
|
||||
small
|
||||
:to="'/streams/' + stream.id + '/branches/main'"
|
||||
>
|
||||
<v-icon small class="mr-2 float-left">mdi-source-commit</v-icon>
|
||||
{{ stream.commits.totalCount }}
|
||||
</v-btn>
|
||||
<v-chip v-if="stream.role" small outlined class="ml-3 no-hover">
|
||||
<v-icon small left>mdi-account-key-outline</v-icon>
|
||||
{{ stream.role.split(':')[1] }}
|
||||
</v-chip>
|
||||
<span class="caption mb-2 ml-3 font-italic">
|
||||
Updated
|
||||
<timeago :datetime="stream.updatedAt"></timeago>
|
||||
</span>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
<!-- BRANCHES -->
|
||||
<v-card
|
||||
v-else-if="lastActivity.resourceType === 'branch'"
|
||||
class="activity-card"
|
||||
:flat="$vuetify.theme.dark"
|
||||
>
|
||||
<v-card-text class="pa-5 body-1">
|
||||
<v-chip :to="url" :color="lastActivityBrief.color">
|
||||
<v-icon small class="mr-2 float-left" light>
|
||||
{{ lastActivityBrief.icon }}
|
||||
</v-icon>
|
||||
{{ branchName }}
|
||||
</v-chip>
|
||||
<span class="ml-3 body-2 font-italic">
|
||||
{{ lastActivityBrief.actionText }}
|
||||
</span>
|
||||
<div class="mt-3">
|
||||
<list-item-activity-description
|
||||
v-for="(item, idx) in activityGroup"
|
||||
:key="item.time"
|
||||
:activity-group="activityGroup"
|
||||
:activity-item-index="idx"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- COMMITS -->
|
||||
<v-card
|
||||
v-else-if="lastActivity.resourceType === 'commit' && commit"
|
||||
class="activity-card"
|
||||
:flat="$vuetify.theme.dark"
|
||||
>
|
||||
<v-container>
|
||||
<v-row class="align-center">
|
||||
<v-col sm="10" cols="12">
|
||||
<v-card-text class="pa-5">
|
||||
<div>
|
||||
<v-chip :to="url" :color="lastActivityBrief.color">
|
||||
<v-icon small class="mr-2 float-left" light>
|
||||
{{ lastActivityBrief.icon }}
|
||||
</v-icon>
|
||||
{{ lastActivity.resourceId }}
|
||||
</v-chip>
|
||||
<span class="mx-3 body-2 font-italic">
|
||||
{{ lastActivityBrief.actionText }}
|
||||
</span>
|
||||
<span v-if="lastActivity.actionType !== 'commit_delete' && commit">
|
||||
<v-chip
|
||||
:to="`/streams/${
|
||||
lastActivity.streamId
|
||||
}/branches/${formatBranchNameForURL(commit.branchName)}`"
|
||||
small
|
||||
color="primary"
|
||||
>
|
||||
<v-icon v-if="commit" small class="float-left" light>
|
||||
mdi-source-branch
|
||||
</v-icon>
|
||||
{{ commit.branchName }}
|
||||
</v-chip>
|
||||
<span v-if="lastActivity.actionType === 'commit_create'">
|
||||
<span class="mx-3 body-2 font-italic">from</span>
|
||||
<source-app-avatar
|
||||
:application-name="commit.sourceApplication"
|
||||
/>
|
||||
</span>
|
||||
<span v-if="lastActivity.actionType === 'commit_receive'">
|
||||
<span class="mx-3 body-2 font-italic">in</span>
|
||||
<source-app-avatar
|
||||
:application-name="lastActivity.info.sourceApplication"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="lastActivity.actionType !== 'commit_delete' && !commit">
|
||||
[commit deleted]
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="lastActivity.info.commit && lastActivity.info.commit.message"
|
||||
class="mt-3 body-1"
|
||||
>
|
||||
{{ lastActivity.info.commit.message }}
|
||||
</div>
|
||||
<!-- NOTE: currently assumes all commits are on the same branch
|
||||
can't easily group them by branch as that info is not in the activity stream -->
|
||||
<router-link
|
||||
v-if="activityGroup.length > 1"
|
||||
:to="`/streams/${
|
||||
lastActivity.streamId
|
||||
}/branches/${formatBranchNameForURL(commit.branchName)}`"
|
||||
class="mt-5 caption"
|
||||
>
|
||||
SEE ALL {{ activityGroup.length }} COMMITS
|
||||
</router-link>
|
||||
</v-card-text>
|
||||
</v-col>
|
||||
|
||||
<v-col sm="2" cols="12">
|
||||
<v-hover
|
||||
v-if="lastActivity.actionType !== 'commit_delete' && commit"
|
||||
v-slot="{ hover }"
|
||||
>
|
||||
<router-link :to="url">
|
||||
<preview-image
|
||||
:url="`/preview/${lastActivity.streamId}/commits/${lastActivity.resourceId}`"
|
||||
:height="100"
|
||||
:color="hover"
|
||||
/>
|
||||
</router-link>
|
||||
</v-hover>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-timeline-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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 '@apollo/client/core'
|
||||
import ListItemActivityDescription from '@/main/components/activity/ListItemActivityDescription.vue'
|
||||
import { STREAM_CREATED_TYPES } from '@/main/lib/feed/helpers/activityStream'
|
||||
import { formatBranchNameForURL } from '@/main/lib/stream/helpers/branches'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserAvatar,
|
||||
SourceAppAvatar,
|
||||
PreviewImage,
|
||||
UserPill,
|
||||
ListItemActivityDescription
|
||||
},
|
||||
props: {
|
||||
activityGroup: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
setup: () => ({ STREAM_CREATED_TYPES, formatBranchNameForURL }),
|
||||
apollo: {
|
||||
you: {
|
||||
query: gql`
|
||||
query {
|
||||
activeUser {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
update: (data) => data.activeUser
|
||||
},
|
||||
user: {
|
||||
query: gql`
|
||||
query ($id: String!) {
|
||||
otherUser(id: $id) {
|
||||
name
|
||||
avatar
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
update: (data) => data.otherUser,
|
||||
variables() {
|
||||
return {
|
||||
id: this.lastActivity.userId
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stream: {
|
||||
query: gql`
|
||||
query ($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
name
|
||||
updatedAt
|
||||
role
|
||||
branches {
|
||||
totalCount
|
||||
}
|
||||
commits {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.lastActivity.streamId
|
||||
}
|
||||
}
|
||||
},
|
||||
branch: {
|
||||
query: gql`
|
||||
query ($id: String!, $branchName: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
branch(name: $branchName) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.lastActivity.streamId,
|
||||
branchName: this.branchName
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return this.lastActivity.resourceType !== 'branch'
|
||||
},
|
||||
update: (data) => data.stream.branch
|
||||
},
|
||||
commit: {
|
||||
query: gql`
|
||||
query ($id: String!, $commitId: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
commit(id: $commitId) {
|
||||
branchName
|
||||
sourceApplication
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: this.lastActivity.streamId,
|
||||
commitId: this.lastActivity.resourceId
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return this.lastActivity.resourceType !== 'commit'
|
||||
},
|
||||
update: (data) => data.stream.commit
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldShowComponent() {
|
||||
if (this.lastActivity.actionType.includes('comment_')) return false
|
||||
if (this.lastActivity.actionType.includes('stream_permissions') && !this.user)
|
||||
return false
|
||||
|
||||
return true
|
||||
},
|
||||
isUserAddedToStreamActivity() {
|
||||
const actionTypes = [
|
||||
'stream_permissions_add',
|
||||
'stream_permissions_invite_accepted'
|
||||
]
|
||||
|
||||
return actionTypes.includes(this.lastActivity?.actionType)
|
||||
},
|
||||
lastActivity() {
|
||||
return this.activityGroup[0]
|
||||
},
|
||||
userName() {
|
||||
return this.user.id === this.you.id ? 'You' : this.user.name
|
||||
},
|
||||
captionText() {
|
||||
return this.lastActivity.actionType.split('_').pop()
|
||||
},
|
||||
branchName() {
|
||||
if (this.lastActivity.info?.branch) return this.lastActivity.info.branch.name
|
||||
else if (this.lastActivity.info?.new?.name) return this.lastActivity.info.new.name
|
||||
else if (this.lastActivity.info?.old?.name) return this.lastActivity.info.old.name
|
||||
return ''
|
||||
},
|
||||
url() {
|
||||
switch (this.lastActivity.resourceType) {
|
||||
case 'stream':
|
||||
return this.stream ? `/streams/${this.lastActivity.streamId}` : null
|
||||
case 'branch':
|
||||
return this.branch
|
||||
? `/streams/${this.lastActivity.streamId}/branches/${formatBranchNameForURL(
|
||||
this.branchName
|
||||
)}`
|
||||
: null
|
||||
case 'commit':
|
||||
return this.commit
|
||||
? `/streams/${this.lastActivity.streamId}/commits/${this.lastActivity.resourceId}`
|
||||
: null
|
||||
case 'user':
|
||||
return '/profile'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
lastActivityBrief() {
|
||||
switch (this.lastActivity.actionType) {
|
||||
case 'stream_create':
|
||||
case 'stream_clone':
|
||||
return {
|
||||
captionText: 'created',
|
||||
actionText: 'new stream'
|
||||
}
|
||||
case 'stream_update':
|
||||
return {
|
||||
captionText: 'updated',
|
||||
actionText: 'stream updated'
|
||||
}
|
||||
case 'stream_delete': //not used
|
||||
return {
|
||||
captionText: 'deleted'
|
||||
}
|
||||
case 'stream_permissions_add':
|
||||
return {
|
||||
captionText: `added ${
|
||||
this.activityGroup.length === 1
|
||||
? 'a user'
|
||||
: this.activityGroup.length + ' users'
|
||||
} to`
|
||||
}
|
||||
case 'stream_permissions_invite_accepted':
|
||||
return {
|
||||
captionText: `accepted an invitation to become a collaborator on`
|
||||
}
|
||||
case 'stream_permissions_remove': {
|
||||
const removedCount = this.activityGroup.length
|
||||
const removedSelf =
|
||||
this.lastActivity.userId === this.lastActivity.info?.targetUser
|
||||
|
||||
if (removedCount > 1) {
|
||||
return {
|
||||
captionText: `removed ${removedCount} users from`
|
||||
}
|
||||
}
|
||||
|
||||
if (removedSelf) {
|
||||
return {
|
||||
captionText: `left the stream`
|
||||
}
|
||||
}
|
||||
|
||||
return { captionText: 'removed a user from' }
|
||||
}
|
||||
case 'branch_create':
|
||||
return {
|
||||
icon: 'mdi-source-branch-plus',
|
||||
captionText: 'created a branch in',
|
||||
actionText: 'new branch',
|
||||
color: 'success'
|
||||
}
|
||||
case 'branch_delete':
|
||||
return {
|
||||
icon: 'mdi-source-branch-minus',
|
||||
captionText: 'deleted a branch from',
|
||||
actionText: 'branch deleted',
|
||||
color: 'error'
|
||||
}
|
||||
case 'branch_update':
|
||||
return {
|
||||
icon: 'mdi-source-branch-sync',
|
||||
captionText: 'updated a branch in',
|
||||
actionText: 'branch updated in',
|
||||
color: 'primary'
|
||||
}
|
||||
case 'commit_create':
|
||||
return {
|
||||
icon: 'mdi-timeline-plus-outline',
|
||||
captionText: `pushed ${this.activityGroup.length} commit${
|
||||
this.activityGroup.length === 0 ? '' : 's'
|
||||
} to`,
|
||||
actionText: 'new commit in',
|
||||
color: 'success'
|
||||
}
|
||||
case 'commit_update':
|
||||
return {
|
||||
icon: 'mdi-timeline-text-outline',
|
||||
captionText: 'updated a commit in',
|
||||
actionText: 'commit updated in',
|
||||
color: 'primary'
|
||||
}
|
||||
case 'commit_receive':
|
||||
return {
|
||||
icon: 'mdi-source-branch-sync',
|
||||
captionText: 'received',
|
||||
actionText: 'commit received from',
|
||||
color: 'primary'
|
||||
}
|
||||
case 'commit_delete':
|
||||
return {
|
||||
icon: 'mdi-timeline-remove-outline',
|
||||
captionText: 'deleted a commit from',
|
||||
color: 'error',
|
||||
actionText: 'commit deleted'
|
||||
}
|
||||
|
||||
case 'user_create':
|
||||
return {
|
||||
icon: 'mdi-account-plus',
|
||||
captionText: 'created'
|
||||
}
|
||||
case 'user_update':
|
||||
return {
|
||||
icon: 'mdi-account-convert',
|
||||
captionText: 'updated'
|
||||
}
|
||||
case 'user_delete':
|
||||
return {
|
||||
icon: 'mdi-account-remove',
|
||||
captionText: 'deleted'
|
||||
}
|
||||
default:
|
||||
return {
|
||||
icon: 'mdi-box',
|
||||
name: this.lastActivity.actionType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.activity-card p {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.timeline-activity a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,133 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="representsCreation">
|
||||
{{ streamDescription }}
|
||||
</template>
|
||||
<template v-else-if="activityItem && activityItem.info && activityItem.info.new">
|
||||
<template v-for="(val, key) in activityItem.info.new">
|
||||
<p v-if="isUpdatedInfoKeyChanged(key)" :key="key">
|
||||
<template v-if="key === UpdatedInfoKeys.Name">
|
||||
✏️ Renamed from
|
||||
<i>
|
||||
<del>
|
||||
{{ activityItem.info.old[key] }}
|
||||
</del>
|
||||
</i>
|
||||
to
|
||||
<i>{{ val }}</i>
|
||||
</template>
|
||||
<template v-else-if="key === UpdatedInfoKeys.Description">
|
||||
📋 Description changed from
|
||||
<i>
|
||||
<del>
|
||||
{{ truncate(activityItem.info.old[key] || 'empty') }}
|
||||
</del>
|
||||
</i>
|
||||
to
|
||||
<i>
|
||||
{{ truncate(val) }}
|
||||
</i>
|
||||
</template>
|
||||
<template v-else-if="key === UpdatedInfoKeys.Message">
|
||||
📋 Message changed from
|
||||
<i>
|
||||
<del>
|
||||
{{ truncate(activityItem.info.old[key] || 'empty') }}
|
||||
</del>
|
||||
</i>
|
||||
to
|
||||
<i>
|
||||
{{ truncate(val) }}
|
||||
</i>
|
||||
</template>
|
||||
<template v-else-if="key === UpdatedInfoKeys.IsPublic">
|
||||
👀 Stream is now
|
||||
<i>
|
||||
{{ val ? 'public' : 'private' }}
|
||||
</i>
|
||||
</template>
|
||||
<template v-else-if="key === UpdatedInfoKeys.IsDiscoverable">
|
||||
📺 Stream is now
|
||||
<i>
|
||||
{{ val ? 'discoverable' : 'not discoverable' }}
|
||||
</i>
|
||||
</template>
|
||||
</p>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
const ActionTypes = {
|
||||
StreamCreate: 'stream_create',
|
||||
BranchCreate: 'branch_create'
|
||||
}
|
||||
|
||||
const UpdatedInfoKeys = {
|
||||
Name: 'name',
|
||||
Description: 'description',
|
||||
Message: 'message',
|
||||
IsPublic: 'isPublic',
|
||||
IsDiscoverable: 'isDiscoverable'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ListItemActivityDescription',
|
||||
props: {
|
||||
activityGroup: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
activityItemIndex: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({ UpdatedInfoKeys }),
|
||||
computed: {
|
||||
activityItem() {
|
||||
return this.activityGroup[this.activityItemIndex]
|
||||
},
|
||||
lastActivityItem() {
|
||||
return this.activityGroup[0]
|
||||
},
|
||||
actionType() {
|
||||
return this.activityItem.actionType
|
||||
},
|
||||
/**
|
||||
* Whether the activity item represents a creation (of a stream/branch)
|
||||
*/
|
||||
representsCreation() {
|
||||
return [ActionTypes.StreamCreate, ActionTypes.BranchCreate].includes(
|
||||
this.activityItem.actionType
|
||||
)
|
||||
},
|
||||
streamDescription() {
|
||||
if (this.activityItem.actionType === ActionTypes.StreamCreate) {
|
||||
return this.activityItem.info?.stream?.description
|
||||
? this.truncate(this.lastActivityItem.info?.stream?.description, 50)
|
||||
: ''
|
||||
} else if (this.activityItem.actionType === ActionTypes.BranchCreate) {
|
||||
return this.activityItem?.info?.branch?.description
|
||||
? this.truncate(this.lastActivityItem.info?.branch?.description, 50)
|
||||
: ''
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
truncate(inputText, length = 25) {
|
||||
return (inputText?.length || 0) > length
|
||||
? inputText.substring(0, length) + '...'
|
||||
: inputText
|
||||
},
|
||||
isUpdatedInfoKeyChanged(key) {
|
||||
const oldVal = this.activityItem.info?.old[key]
|
||||
const newVal = this.activityItem.info?.new[key]
|
||||
|
||||
return oldVal !== undefined && newVal !== oldVal
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<v-chip pill :color="color">
|
||||
<template v-if="targetUser">
|
||||
<v-avatar left>
|
||||
<user-avatar
|
||||
:id="targetUser.id"
|
||||
:avatar="targetUser.avatar"
|
||||
:size="30"
|
||||
:name="targetUser.name"
|
||||
/>
|
||||
</v-avatar>
|
||||
|
||||
{{ targetUser.name }}
|
||||
</template>
|
||||
<template v-else>Deleted user</template>
|
||||
</v-chip>
|
||||
</template>
|
||||
<script>
|
||||
import { gql } from '@apollo/client/core'
|
||||
import UserAvatar from '@/main/components/common/UserAvatar'
|
||||
|
||||
export default {
|
||||
components: { UserAvatar },
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
targetUser: {
|
||||
query: gql`
|
||||
query targetUser($id: String!) {
|
||||
otherUser(id: $id) {
|
||||
name
|
||||
avatar
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
update: (data) => data.otherUser,
|
||||
variables() {
|
||||
return {
|
||||
id: this.userId
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return !this.userId
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,190 +0,0 @@
|
||||
<template>
|
||||
<section-card expandable>
|
||||
<template #header>Usage Stats</template>
|
||||
<v-row v-if="!$apollo.loading" dense class="mt-2">
|
||||
<v-col v-for="value in graphSeries" :key="value.name" cols="12" sm="6">
|
||||
<p class="text-center caption primary--text">
|
||||
<v-icon x-small color="primary" class="mr-1">{{ icons[value.name] }}</v-icon>
|
||||
{{ capitalize(value.name.split('History')[0]) }} history
|
||||
</p>
|
||||
<apex-chart
|
||||
class="primary--text"
|
||||
type="bar"
|
||||
:options="options"
|
||||
:series="[value]"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</section-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { gql } from '@apollo/client/core'
|
||||
import { formatNumber } from '@/plugins/formatNumber.js'
|
||||
|
||||
const EXCLUDED_SERVER_STATS_KEYS = ['__typename']
|
||||
|
||||
export default {
|
||||
name: 'ActivityCard',
|
||||
components: {
|
||||
SectionCard: () => import('@/main/components/common/SectionCard')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
icons: {
|
||||
commitHistory: 'mdi-cloud-upload-outline',
|
||||
streamHistory: 'mdi-cloud-outline',
|
||||
objectHistory: 'mdi-cube-outline',
|
||||
userHistory: 'mdi-account-outline'
|
||||
},
|
||||
options: {
|
||||
states: {
|
||||
active: {
|
||||
filter: {
|
||||
type: 'none' /* none, lighten, darken */
|
||||
}
|
||||
}
|
||||
},
|
||||
chart: {
|
||||
id: 'newUserData',
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
position: 'bottom',
|
||||
formatter(val) {
|
||||
return formatNumber(val)
|
||||
},
|
||||
offsetY: -25,
|
||||
style: {
|
||||
fontSize: '10px',
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
fontWeight: 'bold',
|
||||
colors: undefined
|
||||
},
|
||||
background: {
|
||||
enabled: true,
|
||||
foreColor: '#fff',
|
||||
padding: 6,
|
||||
borderRadius: 5,
|
||||
borderWidth: 2,
|
||||
borderColor: undefined,
|
||||
opacity: 0.9
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
labels: {
|
||||
show: true,
|
||||
rotate: 0,
|
||||
rotateAlways: true,
|
||||
hideOverlappingLabels: true,
|
||||
showDuplicates: false,
|
||||
trim: false,
|
||||
style: {
|
||||
colors: [],
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
fontWeight: 400,
|
||||
cssClass: 'apexcharts-xaxis-label text-center'
|
||||
},
|
||||
offsetX: 0,
|
||||
offsetY: 0
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: false,
|
||||
axisTicks: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
show: false
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 10,
|
||||
columnWidth: '90%',
|
||||
barHeight: '10%',
|
||||
dataLabels: {
|
||||
position: 'top' // top, center, bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
serverStats: {
|
||||
query: gql`
|
||||
query {
|
||||
serverStats {
|
||||
commitHistory
|
||||
objectHistory
|
||||
userHistory
|
||||
streamHistory
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
graphSeries() {
|
||||
let result = []
|
||||
const months = this.past12Months()
|
||||
if (this.serverStats) {
|
||||
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) => {
|
||||
let totalCount = 0
|
||||
category.forEach((value) => {
|
||||
const date = new Date(value.created_month)
|
||||
if (this.isSameMonth(month, date)) {
|
||||
totalCount = value.count
|
||||
}
|
||||
})
|
||||
processed.push([month, totalCount])
|
||||
})
|
||||
return { name: key, data: processed }
|
||||
})
|
||||
}
|
||||
return result.filter((val) => !!val.data)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
capitalize(word) {
|
||||
return word[0].toUpperCase() + word.slice(1).toLowerCase()
|
||||
},
|
||||
past12Months() {
|
||||
const now = new Date(Date.now())
|
||||
const dates = []
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 2)
|
||||
dates.push(d)
|
||||
}
|
||||
return dates
|
||||
},
|
||||
isSameMonth(refDate, date) {
|
||||
return (
|
||||
refDate.getUTCFullYear() === date.getUTCFullYear() &&
|
||||
refDate.getUTCMonth() === date.getUTCMonth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<span v-if="pretty">{{ tweeningValue | prettynum(value) }}</span>
|
||||
<span v-else>{{ tweeningValue }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TWEEN from 'tween'
|
||||
|
||||
export default {
|
||||
name: 'AnimatedNumber',
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
},
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
pretty: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tweeningValue: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(oldVal, newVal) {
|
||||
this.tween(oldVal, newVal)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => this.tween(0, this.value), this.delay)
|
||||
},
|
||||
methods: {
|
||||
tween(startValue, endValue) {
|
||||
const vm = this
|
||||
function animate() {
|
||||
if (TWEEN.update()) {
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
new TWEEN.Tween({ tweeningValue: startValue })
|
||||
.to({ tweeningValue: endValue }, this.duration)
|
||||
.easing(TWEEN.Easing.Quintic.Out)
|
||||
.onUpdate(function () {
|
||||
vm.tweeningValue = this.tweeningValue.toFixed(0)
|
||||
})
|
||||
.start()
|
||||
|
||||
animate()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<section-card expandable>
|
||||
<template #header>General Info</template>
|
||||
<v-row class="d-flex justify-space-around mt-4">
|
||||
<v-col
|
||||
v-for="(value, name) in serverStats"
|
||||
:key="name"
|
||||
cols="6"
|
||||
sm="6"
|
||||
md="3"
|
||||
class="flex-grow-1"
|
||||
>
|
||||
<h4 class="primary--text text--lighten-2 text-center">Total {{ name }}</h4>
|
||||
<v-tooltip bottom color="primary" :disabled="value < 1000">
|
||||
<template #activator="{ on, attrs }">
|
||||
<p
|
||||
class="primary--text text-h3 text-md-h2 text-lg-h1 text-center"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<animated-number :value="value" class="speckle-gradient-txt" />
|
||||
</p>
|
||||
</template>
|
||||
<span>{{ value }}</span>
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</section-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export default {
|
||||
name: 'GeneralInfoCard',
|
||||
components: {
|
||||
AnimatedNumber: () => import('@/main/components/admin/AnimatedNumber'),
|
||||
SectionCard: () => import('@/main/components/common/SectionCard')
|
||||
},
|
||||
apollo: {
|
||||
serverStats: {
|
||||
query: gql`
|
||||
query {
|
||||
serverStats {
|
||||
totalObjectCount
|
||||
totalCommitCount
|
||||
totalStreamCount
|
||||
totalUserCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
update(data) {
|
||||
const stats = data.serverStats
|
||||
return {
|
||||
users: stats.totalUserCount,
|
||||
streams: stats.totalStreamCount,
|
||||
commits: stats.totalCommitCount,
|
||||
objects: stats.totalObjectCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<v-row class="align-center px-4">
|
||||
<v-col cols="3" class="text-truncate">
|
||||
<v-icon v-tooltip="`${stream.isPublic ? 'Public' : 'Private'} stream`" small>
|
||||
{{ stream.isPublic ? 'mdi-lock-open-variant-outline' : 'mdi-lock-outline' }}
|
||||
</v-icon>
|
||||
<router-link
|
||||
class="text-decoration-none space-grotesk mx-1"
|
||||
:to="`/streams/${stream.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ stream.name }}
|
||||
</router-link>
|
||||
</v-col>
|
||||
<v-col cols="2" class="caption text-truncate">
|
||||
Updated
|
||||
<b><timeago :datetime="stream.updatedAt"></timeago></b>
|
||||
<br />
|
||||
<span class="grey--text">
|
||||
({{ new Date(stream.updatedAt).toLocaleString() }})
|
||||
</span>
|
||||
</v-col>
|
||||
<v-col cols="2" class="caption text-truncate">
|
||||
Created
|
||||
<b><timeago :datetime="stream.createdAt"></timeago></b>
|
||||
<br />
|
||||
<span class="grey--text">
|
||||
({{ new Date(stream.createdAt).toLocaleString() }})
|
||||
</span>
|
||||
</v-col>
|
||||
<v-col v-tooltip="'Stream total size'" class="caption font-weight-bold">
|
||||
{{ `${(stream.size ? stream.size / 1048576 : 0.0).toFixed(2)} MB` }}
|
||||
</v-col>
|
||||
<v-col class="caption text-truncate grey--text">
|
||||
<v-icon small>mdi-source-branch</v-icon>
|
||||
{{ stream.branches.totalCount }}
|
||||
<v-icon small>mdi-source-commit</v-icon>
|
||||
{{ stream.commits.totalCount }}
|
||||
</v-col>
|
||||
<v-col class="caption text-truncate">
|
||||
<collaborators-display :stream="stream" :link-to-collabs="false" />
|
||||
</v-col>
|
||||
<v-col cols="1" class="text-right">
|
||||
<v-btn
|
||||
v-tooltip="'Delete stream'"
|
||||
small
|
||||
icon
|
||||
color="error"
|
||||
@click="$emit('delete', stream)"
|
||||
>
|
||||
<v-icon small>mdi-delete-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
// UserAvatar: () => import('@/main/components/common/UserAvatar')
|
||||
CollaboratorsDisplay: () => import('@/main/components/stream/CollaboratorsDisplay')
|
||||
},
|
||||
props: {
|
||||
stream: { type: Object, default: () => null }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<v-row class="d-flex align-center px-3">
|
||||
<v-col cols="6" class="text-truncate">
|
||||
<v-icon color="primary" class="px-2 mr-3">mdi-email</v-icon>
|
||||
<span v-tooltip="invite.email">{{ invite.email }}</span>
|
||||
</v-col>
|
||||
<v-col cols="3" class="text-truncate caption">
|
||||
<span class="grey--text">invited by</span>
|
||||
<user-avatar :id="invite.invitedBy.id" :size="20" class="mx-2" />
|
||||
<span>{{ invite.invitedBy.name }}</span>
|
||||
</v-col>
|
||||
<v-col cols="3" class="d-flex align-center">
|
||||
<v-btn class="flex-grow-1 mr-2" @click="$emit('resend', { inviteId: invite.id })">
|
||||
Resend Invite
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="'Delete invite'"
|
||||
small
|
||||
icon
|
||||
color="error"
|
||||
@click="$emit('delete', { inviteId: invite.id })"
|
||||
>
|
||||
<v-icon small>mdi-delete-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { ServerInvite } from '@/graphql/generated/graphql'
|
||||
import Vue, { PropType } from 'vue'
|
||||
import UserAvatar from '@/main/components/common/UserAvatar.vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'UsersListInviteItem',
|
||||
components: {
|
||||
UserAvatar
|
||||
},
|
||||
props: {
|
||||
invite: {
|
||||
type: Object as PropType<ServerInvite>,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<users-list-user-item
|
||||
v-if="registeredUser"
|
||||
:user="registeredUser"
|
||||
:allow-guest="allowGuest"
|
||||
@change-role="$emit('user-change-role', $event)"
|
||||
@delete="$emit('user-delete', $event)"
|
||||
/>
|
||||
<users-list-invite-item
|
||||
v-else
|
||||
:invite="invitedUser"
|
||||
@delete="$emit('invite-delete', $event)"
|
||||
@resend="$emit('invite-resend', $event)"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { AdminUsersListItem, ServerInvite, User } from '@/graphql/generated/graphql'
|
||||
import Vue, { PropType } from 'vue'
|
||||
import { MaybeFalsy } from '@/helpers/typeHelpers'
|
||||
import UsersListUserItem from '@/main/components/admin/UsersListUserItem.vue'
|
||||
import UsersListInviteItem from '@/main/components/admin/UsersListInviteItem.vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'UsersListItem',
|
||||
components: {
|
||||
UsersListUserItem,
|
||||
UsersListInviteItem
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object as PropType<AdminUsersListItem>,
|
||||
required: true,
|
||||
validator(val: AdminUsersListItem): boolean {
|
||||
return !!(val.invitedUser || val.registeredUser)
|
||||
}
|
||||
},
|
||||
allowGuest: { type: Boolean }
|
||||
},
|
||||
computed: {
|
||||
registeredUser(): MaybeFalsy<User> {
|
||||
return this.item.registeredUser
|
||||
},
|
||||
invitedUser(): MaybeFalsy<ServerInvite> {
|
||||
return this.item.invitedUser
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<v-row class="align-center px-3">
|
||||
<v-col cols="3" class="text-truncate">
|
||||
<user-avatar :id="selfUser.id" :size="30" class="mr-2"></user-avatar>
|
||||
|
||||
<router-link
|
||||
class="text-decoration-none space-grotesk mx-1"
|
||||
:to="`/profile/${selfUser.id}`"
|
||||
>
|
||||
{{ selfUser.name }}
|
||||
</router-link>
|
||||
</v-col>
|
||||
<v-col cols="3" class="caption text-truncate">
|
||||
<v-icon
|
||||
v-if="selfUser.verified"
|
||||
v-tooltip="'Verfied email'"
|
||||
small
|
||||
class="mr-2 primary--text"
|
||||
>
|
||||
mdi-shield-check
|
||||
</v-icon>
|
||||
<v-icon v-else v-tooltip="'Email not verified'" small class="mr-2 warning--text">
|
||||
mdi-shield-alert
|
||||
</v-icon>
|
||||
{{ selfUser.email }}
|
||||
</v-col>
|
||||
<v-col
|
||||
v-tooltip="selfUser.company ? selfUser.company : 'No company info.'"
|
||||
cols="3"
|
||||
class="caption text-truncate"
|
||||
>
|
||||
<v-icon x-small>mdi-domain</v-icon>
|
||||
{{ selfUser.company ? selfUser.company : 'No company info.' }}
|
||||
</v-col>
|
||||
<v-col cols="3" class="d-flex align-center text-right">
|
||||
<v-icon small class="mr-2">
|
||||
{{
|
||||
selfUser.role === serverRoles.Admin
|
||||
? 'mdi-key'
|
||||
: selfUser.role === serverRoles.ArchivedUser
|
||||
? 'mdi-account-off'
|
||||
: 'mdi-account'
|
||||
}}
|
||||
</v-icon>
|
||||
<user-role-select
|
||||
:allow-guest="allowGuest"
|
||||
:role="selfUser.role"
|
||||
@update:role="(e) => $emit('change-role', { user, role: e })"
|
||||
/>
|
||||
<v-btn
|
||||
v-tooltip="'Delete user'"
|
||||
small
|
||||
icon
|
||||
color="error"
|
||||
@click="$emit('delete', selfUser)"
|
||||
>
|
||||
<v-icon small>mdi-delete-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
import UserRoleSelect from '@/main/components/common/UserRoleSelect.vue'
|
||||
import { Roles } from '@speckle/shared'
|
||||
|
||||
export default {
|
||||
name: 'UsersListUserItem',
|
||||
components: {
|
||||
UserAvatar: () => import('@/main/components/common/UserAvatar'),
|
||||
UserRoleSelect
|
||||
},
|
||||
props: {
|
||||
user: { type: Object, default: () => null },
|
||||
allowGuest: { type: Boolean }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selfUser: this.user,
|
||||
serverRoles: Roles.Server
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,96 +0,0 @@
|
||||
<template>
|
||||
<section-card expandable>
|
||||
<template #header>Server Version Info</template>
|
||||
<template #actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
:color="`${isLatestVersion ? 'success' : 'warning'}`"
|
||||
dark
|
||||
href="https://github.com/specklesystems/speckle-server/releases"
|
||||
target="_blank"
|
||||
>
|
||||
<span v-if="isLatestVersion">
|
||||
<v-icon size="medium" class="mb-1">mdi-check-bold</v-icon>
|
||||
Up to date
|
||||
</span>
|
||||
<span v-else>
|
||||
<v-icon size="medium" class="mb-1">mdi-alert</v-icon>
|
||||
Update available
|
||||
</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<div class="d-flex justify-space-around pl-4 pr-4 mt-4">
|
||||
<div>
|
||||
<h4 class="primary--text text--lighten-2">Current</h4>
|
||||
<p class="primary--text text-h4 text-sm-h2 speckle-gradient-txt">
|
||||
{{ versionInfo.current }}
|
||||
</p>
|
||||
</div>
|
||||
<v-icon color="primary lighten-1">mdi-arrow-right</v-icon>
|
||||
<div>
|
||||
<h4 class="primary--text text--lighten-2">Latest</h4>
|
||||
<p class="primary--text text-h4 text-sm-h2 speckle-gradient-txt">
|
||||
{{ versionInfo.latest }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { gql } from '@apollo/client/core'
|
||||
|
||||
export default {
|
||||
name: 'VersionInfoCard',
|
||||
components: { SectionCard: () => import('@/main/components/common/SectionCard') },
|
||||
data() {
|
||||
return {
|
||||
versionInfo: {
|
||||
current: '2.0.18',
|
||||
latest: '2.0.27'
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
currentVersion: {
|
||||
query: gql`
|
||||
query {
|
||||
serverInfo {
|
||||
version
|
||||
}
|
||||
}
|
||||
`,
|
||||
update(data) {
|
||||
this.versionInfo.current = data.serverInfo.version
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isLatestVersion() {
|
||||
return this.versionInfo.current === this.versionInfo.latest
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.versionInfo.latest = await this.getLatestVersion()
|
||||
},
|
||||
methods: {
|
||||
getLatestVersion() {
|
||||
return fetch(
|
||||
'https://api.github.com/repos/specklesystems/speckle-server/releases/latest'
|
||||
)
|
||||
.then(async (res) => {
|
||||
const x = await res.json()
|
||||
return x.tag_name
|
||||
})
|
||||
.catch((err) => {
|
||||
// console.error('error fetch', err)
|
||||
this.$eventHub.$emit('notification', {
|
||||
text: err.message
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<div v-if="strategies && strategies.length !== 0">
|
||||
<v-card-title class="justify-center py-2 body-1 text--secondary">
|
||||
<v-divider class="mx-4"></v-divider>
|
||||
Sign in with
|
||||
<v-divider class="mx-4"></v-divider>
|
||||
</v-card-title>
|
||||
<v-card-text class="pb-5">
|
||||
<template v-for="s in strategies">
|
||||
<v-col
|
||||
:key="s.name"
|
||||
cols="12"
|
||||
class="text-center py-1 my-0"
|
||||
@click="trackSignIn(s.name)"
|
||||
>
|
||||
<v-btn
|
||||
dark
|
||||
block
|
||||
:color="s.color"
|
||||
:href="`${s.url}?appId=${appId}&challenge=${challenge}${
|
||||
token ? '&token=' + token : ''
|
||||
}`"
|
||||
>
|
||||
<v-icon small class="mr-5">{{ s.icon }}</v-icon>
|
||||
{{ s.name }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getInviteTokenFromRoute } from '@/main/lib/auth/services/authService'
|
||||
export default {
|
||||
name: 'AuthStrategies',
|
||||
props: {
|
||||
strategies: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
appId: {
|
||||
type: String,
|
||||
default: () => null
|
||||
},
|
||||
challenge: {
|
||||
type: String,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
token() {
|
||||
return getInviteTokenFromRoute(this.$route)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
trackSignIn(strategyName) {
|
||||
this.$mixpanel.track('Log In', {
|
||||
isInvite: this.token !== null,
|
||||
type: 'action',
|
||||
provider: strategyName
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,96 +0,0 @@
|
||||
<template>
|
||||
<v-card class="elevation-0 transparent pa-5 pb-0">
|
||||
<v-card-text class="text-h3 text-sm-h4 text-md-h3 primary--text">
|
||||
<span class="primary--text">
|
||||
<b>
|
||||
<!-- display: inline -->
|
||||
<a class="text-decoration-none" href="https://speckle.systems" target="_blank"
|
||||
>Speckle</a
|
||||
>
|
||||
</b>
|
||||
</span>
|
||||
<!-- display: inline -->
|
||||
<span class="font-weight-light"
|
||||
>, empowering your design and construction data.</span
|
||||
>
|
||||
</v-card-text>
|
||||
<div v-if="!fe2MessagingEnabled">
|
||||
<v-card-text class="text-h6 font-weight-regular">
|
||||
Speckle helps leading AEC companies freely exchange data between software silos
|
||||
and automate design and delivery processes:
|
||||
<span class="primary--text text--disabled">
|
||||
join 100s of designers, architects, engineers and developers building the
|
||||
digital future of AEC.
|
||||
</span>
|
||||
</v-card-text>
|
||||
</div>
|
||||
<div v-if="fe2MessagingEnabled" class="px-4">
|
||||
<v-divider></v-divider>
|
||||
<v-row align="center" justify="center" class="pt-4 pb-5">
|
||||
<v-col cols="12" class="pb-0">
|
||||
<div class="d-flex align-center">
|
||||
<svg
|
||||
width="22"
|
||||
height="20"
|
||||
viewBox="0 0 22 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.40123 2.0034C9.55572 0.00228626 12.4439 0.00228554 13.5983 2.0034L20.9527 14.7509C22.1065 16.7509 20.6631 19.2501 18.3541 19.2501H3.64546C1.33649 19.2501 -0.106939 16.7509 1.04691 14.7509L8.40123 2.0034ZM11 7.25C11.4142 7.25 11.75 7.58579 11.75 8V11.75C11.75 12.1642 11.4142 12.5 11 12.5C10.5858 12.5 10.25 12.1642 10.25 11.75V8C10.25 7.58579 10.5858 7.25 11 7.25ZM11 15.5C11.4142 15.5 11.75 15.1642 11.75 14.75C11.75 14.3358 11.4142 14 11 14C10.5858 14 10.25 14.3358 10.25 14.75C10.25 15.1642 10.5858 15.5 11 15.5Z"
|
||||
fill="#EAB308"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-h6 font-weight-bold ml-1">This is the Legacy Web App</h3>
|
||||
</div>
|
||||
<p class="mb-0 mt-1 primary--text text--disabled mr-2">
|
||||
A better and more powerful web app is replacing this.
|
||||
</p>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" class="d-flex justify-end">
|
||||
<v-row align="center" justify="center">
|
||||
<v-col cols="12" lg="4" class="d-flex justify-end">
|
||||
<v-btn
|
||||
href="https://speckle.systems/blog/the-new-way-to-collaborate-in-aec/"
|
||||
outlined
|
||||
target="_blank"
|
||||
block
|
||||
class="align-self-center outlined ml-4"
|
||||
>
|
||||
Learn more
|
||||
<v-icon right>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="8" class="d-flex justify-end py-0">
|
||||
<v-btn
|
||||
:href="migrationMovedTo"
|
||||
color="primary"
|
||||
block
|
||||
class="align-self-center outlined ml-4"
|
||||
>
|
||||
Go to the new web app
|
||||
<v-icon right>mdi-rocket-launch</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider></v-divider>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import { useFE2Messaging } from '@/main/lib/core/composables/server'
|
||||
|
||||
export default {
|
||||
name: 'LoginBlurb',
|
||||
setup() {
|
||||
return {
|
||||
...useFE2Messaging()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<div v-if="user" style="display: inline-block" class="text-center">
|
||||
<user-avatar-icon
|
||||
:size="size"
|
||||
:avatar="user.avatar"
|
||||
:seed="user.id"
|
||||
></user-avatar-icon>
|
||||
<p class="text-h6 mt-4">
|
||||
{{ user.name }}
|
||||
<br />
|
||||
<a class="text-body-2" @click="signOut">Not you? Switch accounts.</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { signOut } from '@/plugins/authHelpers'
|
||||
import userQuery from '@/graphql/userById.gql'
|
||||
import UserAvatarIcon from '@/main/components/common/UserAvatarIcon'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
components: { UserAvatarIcon },
|
||||
props: {
|
||||
size: {
|
||||
type: Number,
|
||||
default: 42
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: () => AppLocalStorage.get('uuid')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSelf() {
|
||||
return this.id === AppLocalStorage.get('uuid')
|
||||
},
|
||||
loggedInUserId() {
|
||||
return AppLocalStorage.get('uuid')
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
user: {
|
||||
query: userQuery,
|
||||
variables() {
|
||||
return {
|
||||
id: this.id
|
||||
}
|
||||
},
|
||||
update(data) {
|
||||
return data.otherUser
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
signOut() {
|
||||
signOut(this.$mixpanel)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,251 +0,0 @@
|
||||
<template>
|
||||
<div class="comment-editor">
|
||||
<file-upload-zone
|
||||
ref="uploadZone"
|
||||
v-slot="{ isFileDrag }"
|
||||
:size-limit="blobSizeLimitBytes"
|
||||
:count-limit="countLimit"
|
||||
:accept="acceptValue"
|
||||
:disabled="disabled"
|
||||
multiple
|
||||
@files-selected="onFilesSelected"
|
||||
>
|
||||
<smart-text-editor
|
||||
v-model="doc"
|
||||
:class="['elevation-5 rounded-xl', isFileDrag ? 'dragging-files' : '']"
|
||||
:autofocus="autofocus"
|
||||
min-width
|
||||
:placeholder="placeholder"
|
||||
:schema-options="editorSchemaOptions"
|
||||
:disabled="disabled"
|
||||
:hide-toolbar="addingComment"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</file-upload-zone>
|
||||
<file-upload-progress
|
||||
v-if="currentFiles.length"
|
||||
:items="currentFiles"
|
||||
class="mt-2"
|
||||
:disabled="disabled"
|
||||
@delete="onUploadDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import SmartTextEditor from '@/main/components/common/text-editor/SmartTextEditor.vue'
|
||||
import {
|
||||
CommentEditorValue,
|
||||
SMART_EDITOR_SCHEMA
|
||||
} from '@/main/lib/viewer/comments/commentsHelper'
|
||||
import Vue, { PropType } from 'vue'
|
||||
import FileUploadZone from '@/main/components/common/file-upload/FileUploadZone.vue'
|
||||
import {
|
||||
FilesSelectedEvent,
|
||||
FileUploadDeleteEvent,
|
||||
isUploadProcessed,
|
||||
UniqueFileTypeSpecifier
|
||||
} from '@/main/lib/common/file-upload/fileUploadHelper'
|
||||
import FileUploadProgress from '@/main/components/common/file-upload/FileUploadProgress.vue'
|
||||
import { UploadFileItem } from '@/main/lib/common/file-upload/fileUploadHelper'
|
||||
import { differenceBy } from 'lodash'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { ServerInfoBlobSizeLimitDocument } from '@/graphql/generated/graphql'
|
||||
import { deleteBlob, uploadFiles } from '@/main/lib/common/file-upload/blobStorageApi'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
type FileUploadZoneInstance = InstanceType<typeof FileUploadZone>
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CommentEditor',
|
||||
components: {
|
||||
SmartTextEditor,
|
||||
FileUploadZone,
|
||||
FileUploadProgress
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object as PropType<CommentEditorValue>,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
addingComment: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
streamId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { result } = useQuery(ServerInfoBlobSizeLimitDocument)
|
||||
const blobSizeLimitBytes = computed(
|
||||
() => result.value?.serverInfo.configuration.blobSizeLimitBytes
|
||||
)
|
||||
return { blobSizeLimitBytes }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editorSchemaOptions: SMART_EDITOR_SCHEMA,
|
||||
// fileSizeLimit: 1024 * 1024 * 25, // 25MB
|
||||
countLimit: 5, // if it's more than 5, just zip it up
|
||||
acceptValue: [
|
||||
UniqueFileTypeSpecifier.AnyImage,
|
||||
UniqueFileTypeSpecifier.AnyVideo,
|
||||
'.pdf',
|
||||
'.zip',
|
||||
'.pptx',
|
||||
'.ifc',
|
||||
'.dwg',
|
||||
'.dxf',
|
||||
'.3dm',
|
||||
'.ghx',
|
||||
'.gh',
|
||||
'.rvt',
|
||||
'.pla',
|
||||
'.pln',
|
||||
'.obj',
|
||||
'.blend',
|
||||
'.3ds',
|
||||
'.max',
|
||||
'.mtl',
|
||||
'.stl',
|
||||
'.md',
|
||||
'.txt',
|
||||
'.csv',
|
||||
'.xlsx',
|
||||
'.xls',
|
||||
'.doc',
|
||||
'.docx',
|
||||
'.svg',
|
||||
'.eps',
|
||||
'.gwb',
|
||||
'.skp'
|
||||
].join(',')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
realValue: {
|
||||
get(): CommentEditorValue {
|
||||
return this.value
|
||||
},
|
||||
set(newVal: CommentEditorValue) {
|
||||
this.$emit('input', newVal)
|
||||
}
|
||||
},
|
||||
doc: {
|
||||
get(): JSONContent {
|
||||
return this.value.doc
|
||||
},
|
||||
set(newVal: JSONContent) {
|
||||
this.realValue = {
|
||||
...this.realValue,
|
||||
doc: newVal
|
||||
}
|
||||
}
|
||||
},
|
||||
currentFiles: {
|
||||
get(): UploadFileItem[] {
|
||||
return this.value.attachments
|
||||
},
|
||||
set(newVal: UploadFileItem[]) {
|
||||
this.realValue = {
|
||||
...this.realValue,
|
||||
attachments: newVal
|
||||
}
|
||||
}
|
||||
},
|
||||
placeholder(): string {
|
||||
return 'Press enter to send'
|
||||
},
|
||||
anyAttachmentsProcessing(): boolean {
|
||||
return this.currentFiles.some((a) => !isUploadProcessed(a))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
anyAttachmentsProcessing(newVal: boolean, oldVal: boolean) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$emit('attachments-processing', newVal)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
// Delete attachments that weren't posted
|
||||
for (const currentFile of this.currentFiles.slice()) {
|
||||
if (currentFile.inUse) continue
|
||||
this.popUpload(currentFile.id)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addAttachments(): void {
|
||||
;(this.$refs.uploadZone as FileUploadZoneInstance).triggerPicker()
|
||||
},
|
||||
onSubmit(e: unknown) {
|
||||
this.$emit('submit', e)
|
||||
},
|
||||
onFilesSelected(e: FilesSelectedEvent) {
|
||||
const remainingCount = Math.max(0, this.countLimit - this.currentFiles.length)
|
||||
if (!remainingCount) return
|
||||
|
||||
const incomingFiles = e.files
|
||||
const currentFiles = this.currentFiles
|
||||
const newFiles = differenceBy(incomingFiles, currentFiles, (f) => f.id)
|
||||
if (!newFiles.length) return
|
||||
|
||||
const limitedFiles = newFiles.slice(0, remainingCount)
|
||||
const newUploads = Object.values(
|
||||
uploadFiles(limitedFiles, { streamId: this.streamId }, (uploadedFiles) => {
|
||||
// Delete files that were uploaded, but already removed from attachments
|
||||
for (const [id, file] of Object.entries(uploadedFiles)) {
|
||||
if (
|
||||
file.result?.blobId &&
|
||||
this.currentFiles.findIndex((f) => f.id === id) === -1 &&
|
||||
!file.inUse
|
||||
) {
|
||||
this.deleteBlobInBg(file.result?.blobId)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
this.currentFiles = [...this.currentFiles, ...newUploads]
|
||||
},
|
||||
popUpload(fileId: string) {
|
||||
const fileIdx = this.currentFiles.findIndex((f) => f.id === fileId)
|
||||
if (fileIdx === -1) return
|
||||
|
||||
// Remove from array
|
||||
const [removedFile] = this.currentFiles.splice(fileIdx, 1) || []
|
||||
|
||||
// Delete from blob storage
|
||||
if (removedFile.result?.blobId) {
|
||||
this.deleteBlobInBg(removedFile.result.blobId)
|
||||
}
|
||||
},
|
||||
deleteBlobInBg(blobId: string): void {
|
||||
deleteBlob(blobId, { streamId: this.streamId }).catch(console.error)
|
||||
},
|
||||
onUploadDelete(e: FileUploadDeleteEvent) {
|
||||
const { id } = e
|
||||
this.popUpload(id)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.smart-text-editor) {
|
||||
// transparent border, so we don't get a layout shift
|
||||
border: 2px solid transparent;
|
||||
|
||||
&.dragging-files {
|
||||
border: 2px solid rgb(0, 193, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,287 +0,0 @@
|
||||
<template>
|
||||
<v-card
|
||||
:class="`rounded-lg overflow-hidden ${hovered ? 'elevation-10' : ''} ${
|
||||
isUnread ? 'border' : ''
|
||||
} `"
|
||||
style="transition: box-shadow 0.3s ease"
|
||||
@mouseenter="hovered = true"
|
||||
@mouseleave="hovered = false"
|
||||
>
|
||||
<div v-if="commentDetails" class="">
|
||||
<!-- <v-img :src="commentDetails.screenshot" max-width="150" max-height="150" /> -->
|
||||
<div class="d-flex align-center flex-grow-1 justify-space-between">
|
||||
<div class="mx-2">
|
||||
<user-avatar :id="commentDetails.authorId" :size="40" />
|
||||
</div>
|
||||
<div class="text-truncate body-1 mr-auto">
|
||||
<div class="text-truncate">
|
||||
<router-link class="text-decoration-none" :to="link">
|
||||
{{ documentToBasicString(commentDetails.text.doc) }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="text-truncate caption">
|
||||
<!-- <br /> -->
|
||||
<span v-if="commentDetails.replies.totalCount > 0">
|
||||
<!-- eslint-disable-next-line prettier/prettier -->
|
||||
Last reply
|
||||
<timeago :datetime="commentDetails.updatedAt" />
|
||||
<!--, on {{ new Date(commentDetails.updatedAt).toLocaleString() }} -->
|
||||
<br />
|
||||
</span>
|
||||
<span class="grey--text">
|
||||
Created on {{ new Date(commentDetails.createdAt).toLocaleString() }}
|
||||
</span>
|
||||
<br />
|
||||
<v-btn
|
||||
v-if="canArchiveThread"
|
||||
class="ml-n2 red--text rounded-lg elevation-0"
|
||||
x-small
|
||||
plain
|
||||
@click="showArchiveDialog = true"
|
||||
>
|
||||
Archive
|
||||
</v-btn>
|
||||
<v-dialog v-model="showArchiveDialog" max-width="500">
|
||||
<v-card>
|
||||
<v-toolbar color="error" dark flat>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title>Archive Comment Thread</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="showArchiveDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-card-text class="mt-4">
|
||||
This comment thread will be archived. Are you sure?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="showArchiveDialog = false">Cancel</v-btn>
|
||||
<v-btn color="error" text @click="archiveComment()">Archive</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-btn
|
||||
v-if="isUnread"
|
||||
class="ml-n2 rounded-lg elevation-0"
|
||||
x-small
|
||||
plain
|
||||
@click="markAsRead"
|
||||
>
|
||||
Mark as read
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body-2 px-4 flex-shrink-0">
|
||||
<span
|
||||
v-if="commentDetails.data && commentDetails.data.filters"
|
||||
v-tooltip="`This comment has a filter.`"
|
||||
class="mr-1"
|
||||
>
|
||||
<v-icon small>mdi-filter-variant</v-icon>
|
||||
</span>
|
||||
<span
|
||||
v-if="commentDetails.data && commentDetails.data.sectionBox"
|
||||
v-tooltip="`This comment has a section box.`"
|
||||
class="mr-1"
|
||||
>
|
||||
<v-icon small>mdi-cube-outline</v-icon>
|
||||
</span>
|
||||
|
||||
<v-btn small class="ml-1 primary dark rounded-xl" :to="link">
|
||||
<v-icon small>mdi-comment-outline</v-icon>
|
||||
{{ commentDetails.replies.totalCount }}
|
||||
reply
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<router-link class="text-decoration-none" :to="link">
|
||||
<v-img
|
||||
:src="commentDetails.screenshot"
|
||||
:width="`${$vuetify.breakpoint.xs ? '100' : '200'}`"
|
||||
height="140"
|
||||
:gradient="`to top right, ${
|
||||
$vuetify.theme.dark
|
||||
? 'rgba(100,115,201,.33), rgba(25,32,72,.7)'
|
||||
: 'rgba(100,115,231,.1), rgba(25,32,72,.05)'
|
||||
}`"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import { gql } from '@apollo/client/core'
|
||||
import { documentToBasicString } from '@/main/lib/common/text-editor/documentHelper'
|
||||
import { COMMENT_FULL_INFO_FRAGMENT } from '@/graphql/comments'
|
||||
|
||||
// TODO: Stop polling each comment separately
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserAvatar: () => import('@/main/components/common/UserAvatar')
|
||||
},
|
||||
props: {
|
||||
comment: { type: Object, default: () => null },
|
||||
stream: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return { role: null }
|
||||
}
|
||||
},
|
||||
streamId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
commentDetails: {
|
||||
query: gql`
|
||||
query ($streamId: String!, $id: String!) {
|
||||
comment(streamId: $streamId, id: $id) {
|
||||
...CommentFullInfo
|
||||
}
|
||||
}
|
||||
${COMMENT_FULL_INFO_FRAGMENT}
|
||||
`,
|
||||
fetchPolicy: 'no-cache',
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.streamId,
|
||||
id: this.comment.id
|
||||
}
|
||||
},
|
||||
update(data) {
|
||||
return data.comment
|
||||
},
|
||||
skip() {
|
||||
return !this.comment
|
||||
}
|
||||
},
|
||||
$subscribe: {
|
||||
commentThreadActivity: {
|
||||
query: gql`
|
||||
subscription ($streamId: String!, $commentId: String!) {
|
||||
commentThreadActivity(streamId: $streamId, commentId: $commentId) {
|
||||
type
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.streamId,
|
||||
commentId: this.comment.id
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return !this.$loggedIn()
|
||||
},
|
||||
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()
|
||||
return
|
||||
}
|
||||
if (data.commentThreadActivity.type === 'comment-archived') {
|
||||
this.$emit('deleted', this.comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hovered: false,
|
||||
showArchiveDialog: false,
|
||||
documentToBasicString
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canArchiveThread() {
|
||||
if (!this.comment || !this.stream) return false
|
||||
if (!this.stream.role) return false
|
||||
if (
|
||||
this.comment.authorId === this.$userId() ||
|
||||
this.stream.role === 'stream:owner'
|
||||
)
|
||||
return true
|
||||
return false
|
||||
},
|
||||
link() {
|
||||
if (!this.commentDetails) return
|
||||
const res = this.commentDetails.resources.filter(
|
||||
(r) => r.resourceType !== 'stream'
|
||||
)
|
||||
const first = res.shift()
|
||||
let route = `/streams/${this.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.commentDetails.id}`
|
||||
if (res.length !== 0) {
|
||||
route += `&overlay=${res.map((r) => r.resourceId).join(',')}`
|
||||
}
|
||||
return route
|
||||
},
|
||||
isUnread() {
|
||||
if (!this.commentDetails) return
|
||||
return (
|
||||
new Date(this.commentDetails.updatedAt) -
|
||||
new Date(this.commentDetails.viewedAt) >
|
||||
0
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async markAsRead() {
|
||||
this.commentDetails.viewedAt = Date.now()
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation commentView($streamId: String!, $commentId: String!) {
|
||||
commentView(streamId: $streamId, commentId: $commentId)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
streamId: this.streamId,
|
||||
commentId: this.comment.id
|
||||
}
|
||||
})
|
||||
},
|
||||
async archiveComment() {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation commentArchive($streamId: String!, $commentId: String!) {
|
||||
commentArchive(streamId: $streamId, commentId: $commentId)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
streamId: this.streamId,
|
||||
commentId: this.comment.id
|
||||
}
|
||||
})
|
||||
this.showArchiveDialog = false
|
||||
this.commentDetails.archived = true
|
||||
this.$emit('deleted', this.comment)
|
||||
this.$mixpanel.track('Comment Action', { type: 'action', name: 'archive' })
|
||||
this.$eventHub.$emit('notification', {
|
||||
text: 'Thread archived.'
|
||||
})
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit('notification', {
|
||||
text: e.message
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.border {
|
||||
outline: 2px solid #047efb;
|
||||
}
|
||||
</style>
|
||||
@@ -1,112 +0,0 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-toolbar>
|
||||
<v-toolbar-title>
|
||||
{{ attachment.fileName }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn class="primary" @click="downloadBlob()">
|
||||
<v-icon class="mr-2">mdi-download</v-icon>
|
||||
{{ prettyFileSize(attachment.fileSize) }}
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<template v-if="isImage && !error">
|
||||
<v-img min-width="100%" min-height="100px" :src="blobUrl">
|
||||
<template #placeholder>
|
||||
<v-row class="fill-height ma-0" align="center" justify="center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="grey lighten-5"
|
||||
></v-progress-circular>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-img>
|
||||
</template>
|
||||
<template v-else-if="!error">
|
||||
<v-card-text class="mt-4">
|
||||
<v-icon small class="mr-2">mdi-alert</v-icon>
|
||||
Be cautious when downloading! Attachments are not scanned for harmful content.
|
||||
</v-card-text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-card-text class="mt-4">
|
||||
<v-icon small class="mr-2">mdi-alert</v-icon>
|
||||
Failed to preview attachment.
|
||||
</v-card-text>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import { prettyFileSize } from '@/main/lib/common/file-upload/fileUploadHelper'
|
||||
import {
|
||||
getBlobUrl,
|
||||
downloadBlobWithUrl
|
||||
} from '@/main/lib/common/file-upload/blobStorageApi'
|
||||
import { useCommitObjectViewerParams } from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CommentThreadAttachmentPreview',
|
||||
props: {
|
||||
attachment: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
required: true
|
||||
},
|
||||
isOpen: { type: Boolean, required: true }
|
||||
},
|
||||
setup() {
|
||||
const { streamId, resourceId } = useCommitObjectViewerParams()
|
||||
return { streamId, resourceId }
|
||||
},
|
||||
data: () => ({
|
||||
prettyFileSize,
|
||||
blobUrl: null,
|
||||
error: null
|
||||
}),
|
||||
computed: {
|
||||
isImage() {
|
||||
switch (this.attachment.fileType) {
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'png':
|
||||
case 'gif':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isOpen(val) {
|
||||
if (!val && this.blobUrl) {
|
||||
window.URL.revokeObjectURL(this.blobUrl)
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
if (this.isImage) {
|
||||
this.blobUrl = await getBlobUrl(this.attachment.id, {
|
||||
streamId: this.streamId
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async downloadBlob() {
|
||||
try {
|
||||
const { id, fileName, streamId } = this.attachment
|
||||
await downloadBlobWithUrl(id, fileName, { streamId })
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit('notification', {
|
||||
text: e.message
|
||||
})
|
||||
}
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,196 +0,0 @@
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="d-flex align-center comment-thread-reply"
|
||||
:class="
|
||||
isUserOwned ? 'comment-thread-reply--author' : 'comment-thread-reply--visitor'
|
||||
"
|
||||
@mouseenter="hover = true"
|
||||
@mouseleave="hover = false"
|
||||
>
|
||||
<div
|
||||
:class="`comment-thread-reply__inner flex-grow-1 d-flex flex-column px-2 py-1 mb-2 rounded-xl elevation-2`"
|
||||
style="width: 290px"
|
||||
>
|
||||
<div class="d-flex">
|
||||
<div :class="`d-inline-block`">
|
||||
<user-avatar :id="reply.authorId" :size="30" />
|
||||
</div>
|
||||
<div
|
||||
:class="`reply-box d-inline-block mx-2 py-2 flex-grow-1 float-left caption`"
|
||||
>
|
||||
<smart-text-editor
|
||||
v-if="reply.text.doc"
|
||||
min-width
|
||||
read-only
|
||||
:schema-options="richTextSchema"
|
||||
:value="reply.text.doc"
|
||||
/>
|
||||
<comment-thread-reply-attachments
|
||||
v-if="reply.text.attachments && reply.text.attachments.length"
|
||||
:attachments="reply.text.attachments"
|
||||
:primary="isUserOwned"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 20px; overflow: hidden; position: relative; top: -5px">
|
||||
<v-scroll-x-transition>
|
||||
<v-btn
|
||||
v-show="hover && canArchive"
|
||||
v-tooltip="'Archive'"
|
||||
x-small
|
||||
icon
|
||||
class="ml-1"
|
||||
@click="showArchiveDialog = true"
|
||||
>
|
||||
<v-icon small>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-scroll-x-transition>
|
||||
</div>
|
||||
<v-dialog v-model="showArchiveDialog" max-width="500">
|
||||
<v-card>
|
||||
<v-toolbar color="error" dark flat>
|
||||
<v-app-bar-nav-icon style="pointer-events: none">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
<v-toolbar-title>
|
||||
Archive Comment {{ index === 0 ? 'Thread' : '' }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="showArchiveDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-card-text class="mt-4">
|
||||
This comment {{ index === 0 ? 'thread, including all replies, ' : '' }} will
|
||||
be archived. Are you sure?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="showArchiveDialog = false">Cancel</v-btn>
|
||||
<v-btn color="error" text @click="archiveComment()">Archive</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
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'
|
||||
import { useCommitObjectViewerParams } from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
import { useIsLoggedIn } from '@/main/lib/core/composables/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserAvatar: () => import('@/main/components/common/UserAvatar'),
|
||||
SmartTextEditor,
|
||||
CommentThreadReplyAttachments
|
||||
},
|
||||
props: {
|
||||
reply: { type: Object, default: () => null },
|
||||
stream: { type: Object, default: () => null },
|
||||
index: { type: Number, default: 0 }
|
||||
},
|
||||
setup(props) {
|
||||
const { streamId, resourceId, isEmbed } = useCommitObjectViewerParams()
|
||||
const { userId } = useIsLoggedIn()
|
||||
const isUserOwned = computed(
|
||||
() => !!(userId.value && userId.value === props.reply?.authorId)
|
||||
)
|
||||
|
||||
return { streamId, resourceId, isEmbed, isUserOwned }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hover: false,
|
||||
showArchiveDialog: false,
|
||||
richTextSchema: SMART_EDITOR_SCHEMA
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canArchive() {
|
||||
if (this.isEmbed) return false
|
||||
if (!this.reply || !this.stream) return false
|
||||
if (this.stream.role === 'stream:owner' || this.isUserOwned) return true
|
||||
return false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async archiveComment() {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation commentArchive($streamId: String!, $commentId: String!) {
|
||||
commentArchive(streamId: $streamId, commentId: $commentId)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
streamId: this.streamId,
|
||||
commentId: this.reply.id
|
||||
}
|
||||
})
|
||||
this.$emit('deleted', this.reply.id)
|
||||
this.$mixpanel.track('Comment Action', { type: 'action', name: 'archive' })
|
||||
this.$eventHub.$emit('notification', {
|
||||
text: this.index === 0 ? 'Thread archived.' : 'Comment archived.'
|
||||
})
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit('notification', {
|
||||
text: e.message
|
||||
})
|
||||
}
|
||||
this.showArchiveDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
// Theme-specific coloring
|
||||
.comment-thread-reply {
|
||||
$base: &;
|
||||
|
||||
// Active user's reply
|
||||
&.comment-thread-reply--author {
|
||||
& > #{$base}__inner {
|
||||
background-color: var(--v-primary-base);
|
||||
border-color: var(--v-primary-base);
|
||||
|
||||
&,
|
||||
a {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guest's reply
|
||||
&.comment-thread-reply--visitor {
|
||||
& > #{$base}__inner {
|
||||
background-color: var(--v-background-base);
|
||||
border-color: var(--v-background-base);
|
||||
|
||||
&,
|
||||
a {
|
||||
color: var(--v-text-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.smart-text-editor,
|
||||
.comment-attachments {
|
||||
a {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.smart-text-editor {
|
||||
a:after {
|
||||
content: ' ↗ ';
|
||||
}
|
||||
}
|
||||
</style>
|
||||