Merge branch 'main' into iain/fix-service-account-secrets
This commit is contained in:
Коммит
eb124da8f8
|
@ -16,16 +16,23 @@ workflows:
|
|||
- main
|
||||
- hotfix*
|
||||
|
||||
- test-server:
|
||||
- test-server: &test-server-job-definition
|
||||
context:
|
||||
- speckle-server-licensing
|
||||
- stripe-integration
|
||||
filters: &filters-allow-all
|
||||
tags:
|
||||
# run tests for any commit on any branch, including any tags
|
||||
only: /.*/
|
||||
requires:
|
||||
- docker-publish-postgres-container
|
||||
|
||||
- test-server-no-ff:
|
||||
filters: *filters-allow-all
|
||||
requires:
|
||||
- docker-publish-postgres-container
|
||||
|
||||
- test-server-multiregion: *test-server-job-definition
|
||||
|
||||
- test-frontend-2:
|
||||
filters: *filters-allow-all
|
||||
|
@ -145,6 +152,12 @@ workflows:
|
|||
requires:
|
||||
- get-version
|
||||
|
||||
- docker-build-postgres-container:
|
||||
context: *build-context
|
||||
filters: *filters-build
|
||||
requires:
|
||||
- get-version
|
||||
|
||||
- docker-build-monitor-container:
|
||||
context: *build-context
|
||||
filters: *filters-build
|
||||
|
@ -179,6 +192,7 @@ workflows:
|
|||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-frontend:
|
||||
|
@ -194,6 +208,7 @@ workflows:
|
|||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-frontend-2:
|
||||
|
@ -209,6 +224,7 @@ workflows:
|
|||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-webhooks:
|
||||
|
@ -224,6 +240,7 @@ workflows:
|
|||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-file-imports:
|
||||
|
@ -239,6 +256,7 @@ workflows:
|
|||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-previews:
|
||||
|
@ -254,6 +272,7 @@ workflows:
|
|||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-test-container:
|
||||
|
@ -269,8 +288,15 @@ workflows:
|
|||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-postgres-container:
|
||||
context: *docker-hub-context
|
||||
filters: *filters-publish
|
||||
requires:
|
||||
- docker-build-postgres-container
|
||||
|
||||
- docker-publish-monitor-container:
|
||||
context: *docker-hub-context
|
||||
filters: *filters-publish
|
||||
|
@ -284,6 +310,7 @@ workflows:
|
|||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-docker-compose-ingress:
|
||||
|
@ -299,6 +326,7 @@ workflows:
|
|||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- publish-helm-chart:
|
||||
|
@ -339,6 +367,7 @@ workflows:
|
|||
- get-version
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-ui-components
|
||||
- test-frontend-2
|
||||
- test-viewer
|
||||
|
@ -438,11 +467,12 @@ jobs:
|
|||
docker:
|
||||
- image: cimg/node:18.19.0
|
||||
- image: cimg/redis:7.2.4
|
||||
- image: cimg/postgres:14.11
|
||||
- image: 'speckle/speckle-postgres'
|
||||
environment:
|
||||
POSTGRES_DB: speckle2_test
|
||||
POSTGRES_PASSWORD: speckle
|
||||
POSTGRES_USER: speckle
|
||||
command: -c 'max_connections=1000'
|
||||
- image: 'minio/minio'
|
||||
command: server /data --console-address ":9001"
|
||||
# environment:
|
||||
|
@ -452,6 +482,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'
|
||||
|
@ -464,6 +495,7 @@ jobs:
|
|||
REDIS_URL: 'redis://127.0.0.1:6379'
|
||||
S3_REGION: '' # optional, defaults to 'us-east-1'
|
||||
AUTOMATE_ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
|
||||
FF_BILLING_INTEGRATION_ENABLED: 'true'
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -550,6 +582,52 @@ jobs:
|
|||
FF_WORKSPACES_SSO_ENABLED: 'false'
|
||||
FF_MULTIPLE_EMAILS_MODULE_ENABLED: 'false'
|
||||
FF_GENDOAI_MODULE_ENABLED: 'false'
|
||||
FF_GATEKEEPER_MODULE_ENABLED: 'false'
|
||||
FF_BILLING_INTEGRATION_ENABLED: 'false'
|
||||
|
||||
test-server-multiregion:
|
||||
<<: *test-server-job
|
||||
docker:
|
||||
- image: cimg/node:18.19.0
|
||||
- image: cimg/redis:7.2.4
|
||||
- image: 'speckle/speckle-postgres'
|
||||
environment:
|
||||
POSTGRES_DB: speckle2_test
|
||||
POSTGRES_PASSWORD: speckle
|
||||
POSTGRES_USER: speckle
|
||||
command: -c 'max_connections=1000' -c 'wal_level=logical'
|
||||
- image: 'speckle/speckle-postgres'
|
||||
environment:
|
||||
POSTGRES_DB: speckle2_test
|
||||
POSTGRES_PASSWORD: speckle
|
||||
POSTGRES_USER: speckle
|
||||
command: -c 'max_connections=1000' -c 'port=5433' -c 'wal_level=logical'
|
||||
- image: 'minio/minio'
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
# Same as test-server:
|
||||
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'
|
||||
CANONICAL_URL: 'http://127.0.0.1:3000'
|
||||
S3_ENDPOINT: 'http://127.0.0.1:9000'
|
||||
S3_ACCESS_KEY: 'minioadmin'
|
||||
S3_SECRET_KEY: 'minioadmin'
|
||||
S3_BUCKET: 'speckle-server'
|
||||
S3_CREATE_BUCKET: 'true'
|
||||
REDIS_URL: 'redis://127.0.0.1:6379'
|
||||
S3_REGION: '' # optional, defaults to 'us-east-1'
|
||||
AUTOMATE_ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
|
||||
FF_BILLING_INTEGRATION_ENABLED: 'true'
|
||||
# These are the only different env keys:
|
||||
MULTI_REGION_CONFIG_PATH: '../../.circleci/multiregion.test-ci.json'
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
FF_WORKSPACES_MULTI_REGION_ENABLED: 'true'
|
||||
RUN_TESTS_IN_MULTIREGION_MODE: true
|
||||
|
||||
test-frontend-2:
|
||||
docker: &docker-node-browsers-image
|
||||
|
@ -618,7 +696,7 @@ jobs:
|
|||
test-preview-service:
|
||||
docker:
|
||||
- image: cimg/node:18.19.0
|
||||
- image: cimg/postgres:14.11
|
||||
- image: cimg/postgres:16.4@sha256:2e4f1a965bdd9ba77aa6a0a7b93968c07576ba2a8a7cf86d5eb7b31483db1378
|
||||
environment:
|
||||
POSTGRES_DB: preview_service_test
|
||||
POSTGRES_PASSWORD: preview_service_test
|
||||
|
@ -965,6 +1043,12 @@ jobs:
|
|||
FOLDER: utils
|
||||
SPECKLE_SERVER_PACKAGE: test-deployment
|
||||
|
||||
docker-build-postgres-container:
|
||||
<<: *build-job
|
||||
environment:
|
||||
FOLDER: utils
|
||||
SPECKLE_SERVER_PACKAGE: postgres
|
||||
|
||||
docker-build-monitor-container:
|
||||
<<: *build-job
|
||||
environment:
|
||||
|
@ -1029,6 +1113,12 @@ jobs:
|
|||
FOLDER: utils
|
||||
SPECKLE_SERVER_PACKAGE: test-deployment
|
||||
|
||||
docker-publish-postgres-container:
|
||||
<<: *publish-job
|
||||
environment:
|
||||
FOLDER: utils
|
||||
SPECKLE_SERVER_PACKAGE: postgres
|
||||
|
||||
docker-publish-monitor-container:
|
||||
<<: *publish-job
|
||||
environment:
|
||||
|
@ -1117,6 +1207,7 @@ jobs:
|
|||
publish-viewer-sandbox-cloudflare-pages:
|
||||
docker: *docker-node-image
|
||||
working_directory: *work-dir
|
||||
resource_class: large
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
|
|
@ -45,11 +45,12 @@ k8s_yaml('./manifests/priorityclass.yaml')
|
|||
k8s_yaml('./manifests/speckle-server.secret.yaml')
|
||||
|
||||
# Install charts
|
||||
# Postgres 16.4 is packaged in chart 15.5.38
|
||||
helm_resource('postgresql',
|
||||
release_name='postgresql',
|
||||
namespace='postgres',
|
||||
chart='oci://registry-1.docker.io/bitnamicharts/postgresql',
|
||||
flags=['--version=^12.0.0',
|
||||
flags=['--version=^15.5.38',
|
||||
'--values=./values/postgres.values.yaml',
|
||||
'--kube-context=kind-speckle-server'],
|
||||
deps=['./values/postgres.values.yaml'],
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"main": {
|
||||
"postgres": {
|
||||
"connectionUri": "postgresql://speckle:speckle@127.0.0.1:5432/speckle2_test"
|
||||
}
|
||||
},
|
||||
"regions": {
|
||||
"region1": {
|
||||
"postgres": {
|
||||
"connectionUri": "postgresql://speckle:speckle@127.0.0.1:5433/speckle2_test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -69,7 +69,7 @@ jobs:
|
|||
services:
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres:14
|
||||
image: postgres:16.4-bookworm@sha256:91f464e7ba0ad91a106c94cff079fb4384139291b8c0502fd36989cf2c788bbb
|
||||
env:
|
||||
POSTGRES_DB: preview_service_test
|
||||
POSTGRES_PASSWORD: preview_service_test
|
||||
|
|
|
@ -71,4 +71,8 @@ minio-data/
|
|||
postgres-data/
|
||||
redis-data/
|
||||
|
||||
.tshy-build
|
||||
.tshy-build
|
||||
|
||||
# Server
|
||||
multiregion.json
|
||||
multiregion.test.json
|
43
README.md
43
README.md
|
@ -2,12 +2,14 @@
|
|||
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
|
||||
Speckle | Server
|
||||
</h1>
|
||||
|
||||
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&style=flat-square&logo=discourse&logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white" alt="docs"></a></p>
|
||||
|
||||
> Speckle is the first AEC data hub that connects with your favorite AEC tools. Speckle exists to overcome the challenges of working in a fragmented industry where communication, creative workflows, and the exchange of data are often hindered by siloed software and processes. It is here to make the industry better.
|
||||
|
||||
<h3 align="center">
|
||||
Server and Web packages
|
||||
</h3>
|
||||
<p align="center"><b>Speckle</b> is data infrastructure for the AEC industry.</p><br/>
|
||||
|
||||
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&style=flat-square&logo=discourse&logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white" alt="docs"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://codecov.io/gh/specklesystems/speckle-server">
|
||||
|
@ -18,37 +20,6 @@
|
|||
</a>
|
||||
</p>
|
||||
|
||||
# About Speckle
|
||||
|
||||
What is Speckle? Check our [![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)](https://www.youtube.com/watch?v=B9humiSpHzM)
|
||||
|
||||
## Features
|
||||
|
||||
- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry
|
||||
- **Version control:** Speckle is the Git & Hub for geometry and BIM data
|
||||
- **Collaboration:** share your designs collaborate with others
|
||||
- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere
|
||||
- **Connectivity:** get your CAD and BIM models into other software without exporting or importing
|
||||
- **Real time:** get real time updates and notifications and changes
|
||||
- **GraphQL API:** get what you need anywhere you want it
|
||||
- **Webhooks:** the base for a automation and next-gen pipelines
|
||||
- **Built for developers:** we are building Speckle with developers in mind and have tools for every stack
|
||||
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender, ArchiCAD and more!
|
||||
|
||||
## Try Speckle now!
|
||||
|
||||
Give Speckle a try in no time by:
|
||||
|
||||
- [![app.speckle.systems](https://img.shields.io/badge/https://-app.speckle.systems-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://app.speckle.systems) ⇒ Create an account at app.speckle.systems
|
||||
- [![Deploy on your own infrastructure with docker compose](https://img.shields.io/badge/https://-speckle.guide-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](<[https://](https://speckle.guide/dev/server-manualsetup.html)>) ⇒ Deploy on your own infrastructure with Docker Compose
|
||||
- [![Deploy on your own infrastructure with docker compose](https://img.shields.io/badge/https://-speckle.guide-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](<[https://](https://speckle.guide/dev/server-setup-k8s.html)>) ⇒ Deploy on your own infrastructure with Kubernetes
|
||||
|
||||
## Resources
|
||||
|
||||
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
|
||||
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
|
||||
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
|
||||
|
||||
# Repo structure
|
||||
|
||||
This monorepo is the home of the Speckle v2 web packages:
|
||||
|
@ -65,8 +36,10 @@ This monorepo is the home of the Speckle v2 web packages:
|
|||
|
||||
Make sure to also check and ⭐️ these other Speckle repositories:
|
||||
|
||||
- [`speckle-sharp`](https://github.com/specklesystems/speckle-sharp): .NET tooling, connectors and interoperability
|
||||
- [`speckle-sharp-connectors`](https://github.com/specklesystems/speckle-sharp-connectors): .NET connectors and desktop UI
|
||||
- [`speckle-sharp-sdk`](https://github.com/specklesystems/speckle-sharp-sdk): .NET SDK, tests, and Objects
|
||||
- [`specklepy`](https://github.com/specklesystems/specklepy): Python SDK 🐍
|
||||
- [`speckle-sketchup`](https://github.com/specklesystems/speckle-sketchup): Sketchup connector
|
||||
- [`speckle-excel`](https://github.com/specklesystems/speckle-excel): Excel connector
|
||||
- [`speckle-unity`](https://github.com/specklesystems/speckle-unity): Unity 3D connector
|
||||
- [`speckle-blender`](https://github.com/specklesystems/speckle-blender): Blender connector
|
||||
|
|
|
@ -5,7 +5,7 @@ services:
|
|||
postgres:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/postgres/Dockerfile
|
||||
dockerfile: utils/postgres/Dockerfile
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: speckle
|
||||
|
@ -18,6 +18,22 @@ services:
|
|||
ports:
|
||||
- '127.0.0.1:5432:5432'
|
||||
|
||||
postgres-region1:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: utils/postgres/Dockerfile
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: speckle
|
||||
POSTGRES_USER: speckle
|
||||
POSTGRES_PASSWORD: speckle
|
||||
volumes:
|
||||
- postgres-region1-data:/var/lib/postgresql/data/
|
||||
- ./setup/db/10-docker_postgres_init.sql:/docker-entrypoint-initdb.d/10-docker_postgres_init.sql
|
||||
- ./setup/db/11-docker_postgres_keycloack_init.sql:/docker-entrypoint-initdb.d/11-docker_postgres_keycloack_init.sql
|
||||
ports:
|
||||
- '127.0.0.1:5401:5432'
|
||||
|
||||
redis:
|
||||
image: 'redis:7-alpine'
|
||||
restart: always
|
||||
|
@ -106,6 +122,7 @@ services:
|
|||
|
||||
volumes:
|
||||
postgres-data:
|
||||
postgres-region1-data:
|
||||
redis-data:
|
||||
pgadmin-data:
|
||||
redis_insight-data:
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
FROM postgres:14.5-alpine as builder
|
||||
|
||||
RUN apk add --no-cache 'git=~2.36' \
|
||||
'build-base=~0.5' \
|
||||
'clang=~13.0' \
|
||||
'llvm13=~13.0'
|
||||
|
||||
WORKDIR /
|
||||
RUN git clone --branch 1.1.9 https://github.com/aiven/aiven-extras.git aiven-extras
|
||||
|
||||
WORKDIR /aiven-extras
|
||||
RUN git checkout 36598ab \
|
||||
&& git clean -df \
|
||||
&& make \
|
||||
&& make install
|
||||
|
||||
FROM postgres:14.5-alpine
|
||||
|
||||
COPY --from=builder /aiven-extras/aiven_extras.control /usr/local/share/postgresql/extension/aiven_extras.control
|
||||
COPY --from=builder /aiven-extras/sql/aiven_extras.sql /usr/local/share/postgresql/extension/aiven_extras--1.1.9.sql
|
||||
COPY --from=builder /aiven-extras/aiven_extras.so /usr/local/lib/postgresql/aiven_extras.so
|
||||
|
||||
EXPOSE 5432
|
||||
|
||||
CMD ["postgres"]
|
|
@ -21,6 +21,7 @@
|
|||
"dev:docker": "docker compose -f ./docker-compose-deps.yml",
|
||||
"dev:docker:up": "docker compose -f ./docker-compose-deps.yml up -d",
|
||||
"dev:docker:down": "docker compose -f ./docker-compose-deps.yml down",
|
||||
"dev:docker:restart": "yarn dev:docker:down && yarn dev:docker:up",
|
||||
"dev:kind:up": "ctlptl apply --filename ./.circleci/deployment/cluster-config.yaml",
|
||||
"dev:kind:down": "ctlptl delete -f ./.circleci/deployment/cluster-config.yaml",
|
||||
"dev:kind:helm:up": "yarn dev:kind:up && tilt up --file ./.circleci/deployment/Tiltfile.helm --context kind-speckle-server",
|
||||
|
|
|
@ -52,7 +52,7 @@ export type Activity = {
|
|||
export type ActivityCollection = {
|
||||
__typename?: 'ActivityCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
items?: Maybe<Array<Maybe<Activity>>>;
|
||||
items: Array<Activity>;
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
|
@ -1766,6 +1766,7 @@ export type PendingWorkspaceCollaborator = {
|
|||
user?: Maybe<LimitedUser>;
|
||||
workspaceId: Scalars['String']['output'];
|
||||
workspaceName: Scalars['String']['output'];
|
||||
workspaceSlug: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type PendingWorkspaceCollaboratorsFilter = {
|
||||
|
@ -2491,6 +2492,7 @@ export type Query = {
|
|||
/** Validates the slug, to make sure it contains only valid characters and its not taken. */
|
||||
validateWorkspaceSlug: Scalars['Boolean']['output'];
|
||||
workspace: Workspace;
|
||||
workspaceBySlug: Workspace;
|
||||
/**
|
||||
* Look for an invitation to a workspace, for the current user (authed or not).
|
||||
*
|
||||
|
@ -2635,7 +2637,13 @@ export type QueryWorkspaceArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type QueryWorkspaceBySlugArgs = {
|
||||
slug: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryWorkspaceInviteArgs = {
|
||||
options?: InputMaybe<WorkspaceInviteLookupOptions>;
|
||||
token?: InputMaybe<Scalars['String']['input']>;
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
@ -4003,6 +4011,11 @@ export type WorkspaceInviteCreateInput = {
|
|||
userId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type WorkspaceInviteLookupOptions = {
|
||||
/** If true, the query will assume workspaceId is actually the workspace slug, and do the lookup by slug */
|
||||
useSlug?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
};
|
||||
|
||||
export type WorkspaceInviteMutations = {
|
||||
__typename?: 'WorkspaceInviteMutations';
|
||||
batchCreate: Workspace;
|
||||
|
@ -4163,8 +4176,8 @@ export type WorkspaceRoleUpdateInput = {
|
|||
};
|
||||
|
||||
export type WorkspaceTeamFilter = {
|
||||
/** Limit team members to provided role */
|
||||
role?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Limit team members to provided role(s) */
|
||||
roles?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
/** Search for team members by name or email */
|
||||
search?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch via NPM",
|
||||
"name": "Launch via Yarn",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["run-script", "dev"],
|
||||
"runtimeExecutable": "npm",
|
||||
"console": "integratedTerminal",
|
||||
"runtimeArgs": ["dev"],
|
||||
"runtimeExecutable": "yarn",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"type": "node"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -5,17 +5,21 @@ const bcrypt = require('bcrypt')
|
|||
const { chunk } = require('lodash')
|
||||
const { logger: parentLogger } = require('../observability/logging')
|
||||
|
||||
const knex = require('../knex')
|
||||
const Observability = require('@speckle/shared/dist/commonjs/observability/index.js')
|
||||
const Streams = () => knex('streams')
|
||||
const Branches = () => knex('branches')
|
||||
const Objects = () => knex('objects')
|
||||
const Closures = () => knex('object_children_closure')
|
||||
const ApiTokens = () => knex('api_tokens')
|
||||
const TokenScopes = () => knex('token_scopes')
|
||||
|
||||
const tables = (db) => ({
|
||||
objects: db('objects'),
|
||||
closures: db('object_children_closure'),
|
||||
branches: db('branches'),
|
||||
streams: db('streams'),
|
||||
apiTokens: db('api_tokens'),
|
||||
tokenScopes: db('token_scopes')
|
||||
})
|
||||
|
||||
module.exports = class ServerAPI {
|
||||
constructor({ streamId, logger }) {
|
||||
constructor({ db, streamId, logger }) {
|
||||
this.tables = tables(db)
|
||||
this.db = db
|
||||
this.streamId = streamId
|
||||
this.isSending = false
|
||||
this.buffer = []
|
||||
|
@ -68,10 +72,10 @@ module.exports = class ServerAPI {
|
|||
totalChildrenCountByDepth
|
||||
)
|
||||
|
||||
await Objects().insert(insertionObject).onConflict().ignore()
|
||||
await this.tables.objects.insert(insertionObject).onConflict().ignore()
|
||||
|
||||
if (closures.length > 0) {
|
||||
await Closures().insert(closures).onConflict().ignore()
|
||||
await this.tables.closures.insert(closures).onConflict().ignore()
|
||||
}
|
||||
|
||||
return insertionObject.id
|
||||
|
@ -123,7 +127,7 @@ module.exports = class ServerAPI {
|
|||
const batches = chunk(objsToInsert, objectsBatchSize)
|
||||
for (const [index, batch] of batches.entries()) {
|
||||
this.prepInsertionObjectBatch(batch)
|
||||
await Objects().insert(batch).onConflict().ignore()
|
||||
await this.tables.objects.insert(batch).onConflict().ignore()
|
||||
this.logger.info(
|
||||
{
|
||||
currentBatchCount: batch.length,
|
||||
|
@ -141,7 +145,7 @@ module.exports = class ServerAPI {
|
|||
|
||||
for (const [index, batch] of batches.entries()) {
|
||||
this.prepInsertionClosureBatch(batch)
|
||||
await Closures().insert(batch).onConflict().ignore()
|
||||
await this.tables.closures.insert(batch).onConflict().ignore()
|
||||
this.logger.info(
|
||||
{
|
||||
currentBatchCount: batch.length,
|
||||
|
@ -196,10 +200,10 @@ module.exports = class ServerAPI {
|
|||
}
|
||||
|
||||
async getBranchByNameAndStreamId({ streamId, name }) {
|
||||
const query = Branches()
|
||||
const query = this.tables.branches
|
||||
.select('*')
|
||||
.where({ streamId })
|
||||
.andWhere(knex.raw('LOWER(name) = ?', [name]))
|
||||
.andWhere(this.db.raw('LOWER(name) = ?', [name]))
|
||||
.first()
|
||||
return await query
|
||||
}
|
||||
|
@ -212,10 +216,12 @@ module.exports = class ServerAPI {
|
|||
branch.name = name.toLowerCase()
|
||||
branch.description = description
|
||||
|
||||
await Branches().returning('id').insert(branch)
|
||||
await this.tables.branches.returning('id').insert(branch)
|
||||
|
||||
// update stream updated at
|
||||
await Streams().where({ id: streamId }).update({ updatedAt: knex.fn.now() })
|
||||
await this.tables.streams
|
||||
.where({ id: streamId })
|
||||
.update({ updatedAt: this.db.fn.now() })
|
||||
|
||||
return branch.id
|
||||
}
|
||||
|
@ -244,14 +250,14 @@ module.exports = class ServerAPI {
|
|||
}
|
||||
const tokenScopes = scopes.map((scope) => ({ tokenId, scopeName: scope }))
|
||||
|
||||
await ApiTokens().insert(token)
|
||||
await TokenScopes().insert(tokenScopes)
|
||||
await this.tables.apiTokens.insert(token)
|
||||
await this.tables.tokenScopes.insert(tokenScopes)
|
||||
|
||||
return { id: tokenId, token: tokenId + tokenString }
|
||||
}
|
||||
|
||||
async revokeTokenById(tokenId) {
|
||||
const delCount = await ApiTokens()
|
||||
const delCount = await this.tables.apiTokens
|
||||
.where({ id: tokenId.slice(0, 10) })
|
||||
.del()
|
||||
|
||||
|
|
|
@ -3,13 +3,15 @@ const { logger: parentLogger } = require('../observability/logging')
|
|||
|
||||
const TMP_RESULTS_PATH = '/tmp/import_result.json'
|
||||
|
||||
const { parseAndCreateCommit } = require('./index')
|
||||
const { parseAndCreateCommitFactory } = require('./index')
|
||||
const Observability = require('@speckle/shared/dist/commonjs/observability/index.js')
|
||||
const getDbClients = require('../knex')
|
||||
|
||||
async function main() {
|
||||
const cmdArgs = process.argv.slice(2)
|
||||
|
||||
const [filePath, userId, streamId, branchName, commitMessage, fileId] = cmdArgs
|
||||
const [filePath, userId, streamId, branchName, commitMessage, fileId, regionName] =
|
||||
cmdArgs
|
||||
const logger = Observability.extendLoggerComponent(
|
||||
parentLogger.child({ streamId, branchName, userId, fileId, filePath }),
|
||||
'ifc'
|
||||
|
@ -34,8 +36,10 @@ async function main() {
|
|||
error: 'Unknown error'
|
||||
}
|
||||
|
||||
const dbClients = await getDbClients()
|
||||
const knex = dbClients[regionName].public
|
||||
try {
|
||||
const commitId = await parseAndCreateCommit(ifcInput)
|
||||
const commitId = await parseAndCreateCommitFactory({ db: knex })(ifcInput)
|
||||
output = {
|
||||
success: true,
|
||||
commitId
|
||||
|
|
|
@ -1,86 +1,88 @@
|
|||
const { performance } = require('perf_hooks')
|
||||
const { fetch } = require('undici')
|
||||
const Parser = require('./parser_v2')
|
||||
const Parser = require('./parser')
|
||||
const ServerAPI = require('./api.js')
|
||||
const Observability = require('@speckle/shared/dist/commonjs/observability/index.js')
|
||||
const { logger: parentLogger } = require('../observability/logging')
|
||||
|
||||
async function parseAndCreateCommit({
|
||||
data,
|
||||
streamId,
|
||||
branchName = 'uploads',
|
||||
userId,
|
||||
message = 'Manual IFC file upload',
|
||||
fileId,
|
||||
logger
|
||||
}) {
|
||||
if (!logger) {
|
||||
logger = Observability.extendLoggerComponent(
|
||||
parentLogger.child({ streamId, branchName, userId, fileId }),
|
||||
'ifc'
|
||||
const parseAndCreateCommitFactory =
|
||||
({ db }) =>
|
||||
async ({
|
||||
data,
|
||||
streamId,
|
||||
branchName = 'uploads',
|
||||
userId,
|
||||
message = 'Manual IFC file upload',
|
||||
fileId,
|
||||
logger
|
||||
}) => {
|
||||
if (!logger) {
|
||||
logger = Observability.extendLoggerComponent(
|
||||
parentLogger.child({ streamId, branchName, userId, fileId }),
|
||||
'ifc'
|
||||
)
|
||||
}
|
||||
const serverApi = new ServerAPI({ db, streamId, logger })
|
||||
const myParser = new Parser({ serverApi, fileId, logger })
|
||||
|
||||
const start = performance.now()
|
||||
const { id, tCount } = await myParser.parse(data)
|
||||
logger = logger.child({ objectId: id })
|
||||
const end = performance.now()
|
||||
logger.info(
|
||||
{
|
||||
fileProcessingDurationMs: (end - start).toFixed(2)
|
||||
},
|
||||
'Total processing time V2: {fileProcessingDurationMs}ms'
|
||||
)
|
||||
}
|
||||
const serverApi = new ServerAPI({ streamId, logger })
|
||||
const myParser = new Parser({ serverApi, fileId, logger })
|
||||
|
||||
const start = performance.now()
|
||||
const { id, tCount } = await myParser.parse(data)
|
||||
logger = logger.child({ objectId: id })
|
||||
const end = performance.now()
|
||||
logger.info(
|
||||
{
|
||||
fileProcessingDurationMs: (end - start).toFixed(2)
|
||||
},
|
||||
'Total processing time V2: {fileProcessingDurationMs}ms'
|
||||
)
|
||||
|
||||
const commit = {
|
||||
streamId,
|
||||
branchName,
|
||||
objectId: id,
|
||||
message,
|
||||
sourceApplication: 'IFC',
|
||||
totalChildrenCount: tCount
|
||||
}
|
||||
|
||||
const branch = await serverApi.getBranchByNameAndStreamId({
|
||||
streamId,
|
||||
name: branchName
|
||||
})
|
||||
|
||||
if (!branch) {
|
||||
logger.info("Branch '{branchName}' not found, creating it.")
|
||||
await serverApi.createBranch({
|
||||
name: branchName,
|
||||
const commit = {
|
||||
streamId,
|
||||
description: branchName === 'uploads' ? 'File upload branch' : null,
|
||||
authorId: userId
|
||||
branchName,
|
||||
objectId: id,
|
||||
message,
|
||||
sourceApplication: 'IFC',
|
||||
totalChildrenCount: tCount
|
||||
}
|
||||
|
||||
const branch = await serverApi.getBranchByNameAndStreamId({
|
||||
streamId,
|
||||
name: branchName
|
||||
})
|
||||
|
||||
if (!branch) {
|
||||
logger.info("Branch '{branchName}' not found, creating it.")
|
||||
await serverApi.createBranch({
|
||||
name: branchName,
|
||||
streamId,
|
||||
description: branchName === 'uploads' ? 'File upload branch' : null,
|
||||
authorId: userId
|
||||
})
|
||||
}
|
||||
|
||||
const userToken = process.env.USER_TOKEN
|
||||
|
||||
const serverBaseUrl = process.env.SPECKLE_SERVER_URL || 'http://127.0.0.1:3000'
|
||||
logger.info(`Creating commit for object ({objectId}), with message "${message}"`)
|
||||
const response = await fetch(serverBaseUrl + '/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${userToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query:
|
||||
'mutation createCommit( $myCommitInput: CommitCreateInput!) { commitCreate( commit: $myCommitInput ) }',
|
||||
variables: {
|
||||
myCommitInput: commit
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
logger.info(json, 'Commit created')
|
||||
|
||||
return json.data.commitCreate
|
||||
}
|
||||
|
||||
const userToken = process.env.USER_TOKEN
|
||||
|
||||
const serverBaseUrl = process.env.SPECKLE_SERVER_URL || 'http://127.0.0.1:3000'
|
||||
logger.info(`Creating commit for object ({objectId}), with message "${message}"`)
|
||||
const response = await fetch(serverBaseUrl + '/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${userToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query:
|
||||
'mutation createCommit( $myCommitInput: CommitCreateInput!) { commitCreate( commit: $myCommitInput ) }',
|
||||
variables: {
|
||||
myCommitInput: commit
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
logger.info(json, 'Commit created')
|
||||
|
||||
return json.data.commitCreate
|
||||
}
|
||||
|
||||
module.exports = { parseAndCreateCommit }
|
||||
module.exports = { parseAndCreateCommitFactory }
|
||||
|
|
|
@ -1,328 +1,403 @@
|
|||
/* eslint-disable camelcase */
|
||||
const { performance } = require('perf_hooks')
|
||||
const WebIFC = require('web-ifc/web-ifc-api-node')
|
||||
const { logger } = require('../observability/logging.js')
|
||||
const ServerAPI = require('./api.js')
|
||||
const {
|
||||
getHash,
|
||||
IfcElements,
|
||||
PropNames,
|
||||
GeometryTypes,
|
||||
IfcTypesMap
|
||||
} = require('./utils')
|
||||
const Observability = require('@speckle/shared/dist/commonjs/observability/index.js')
|
||||
const { logger: parentLogger } = require('../observability/logging')
|
||||
|
||||
module.exports = class IFCParser {
|
||||
constructor({ serverApi, logger }) {
|
||||
this.api = new WebIFC.IfcAPI()
|
||||
this.serverApi = serverApi || new ServerAPI({ logger })
|
||||
constructor({ serverApi, fileId, logger }) {
|
||||
this.ifcapi = new WebIFC.IfcAPI()
|
||||
this.ifcapi.SetWasmPath('./', false)
|
||||
this.serverApi = serverApi
|
||||
this.fileId = fileId
|
||||
this.logger =
|
||||
logger ||
|
||||
Observability.extendLoggerComponent(parentLogger.child({ fileId }), 'ifc')
|
||||
}
|
||||
|
||||
async parse(data) {
|
||||
if (this.api.wasmModule === undefined) await this.api.Init()
|
||||
|
||||
this.modelId = this.api.OpenModel(data, {
|
||||
COORDINATE_TO_ORIGIN: true,
|
||||
await this.ifcapi.Init()
|
||||
this.modelId = this.ifcapi.OpenModel(new Uint8Array(data), {
|
||||
USE_FAST_BOOLS: true
|
||||
})
|
||||
|
||||
this.projectId = this.api.GetLineIDsWithType(this.modelId, WebIFC.IFCPROJECT).get(0)
|
||||
this.startTime = performance.now()
|
||||
|
||||
this.project = this.api.GetLine(this.modelId, this.projectId, true)
|
||||
this.project.__closure = {}
|
||||
// prepoulate types
|
||||
this.types = await this.getAllTypesOfModel()
|
||||
|
||||
this.cache = {}
|
||||
this.closureCache = {}
|
||||
// prime caches for property sets and their relating objects, as well as,
|
||||
// most importantly, all the properties.
|
||||
const { psetLines, psetRelations, properties } = await this.getAllProps()
|
||||
this.psetLines = psetLines
|
||||
this.psetRelations = psetRelations
|
||||
this.properties = properties
|
||||
|
||||
// Steps: create and store in speckle all the geometries (meshes) from this project and store them
|
||||
// as reference objects in this.productGeo
|
||||
this.productGeo = {}
|
||||
await this.createGeometries()
|
||||
logger.info(`Geometries created: ${Object.keys(this.productGeo).length} meshes.`)
|
||||
this.propCache = {}
|
||||
|
||||
// Lastly, traverse the ifc project object and parse it into something friendly; as well as
|
||||
// replace all its geometries with actual references to speckle meshes from the productGeo map
|
||||
// This is used to pre-batch ifc objects that need to be persisted.
|
||||
this.objectBucket = []
|
||||
|
||||
await this.traverse(this.project, true, 0)
|
||||
// create and save the geometries; we're storing only references locally.
|
||||
this.geometryReferences = await this.createAndSaveMeshes()
|
||||
|
||||
const id = await this.serverApi.saveObject(this.project)
|
||||
return { id, tCount: Object.keys(this.project.__closure).length }
|
||||
// create and save the spatial tree, populating both properties and geometry references
|
||||
// where appropriate
|
||||
this.spatialNodeCount = 0
|
||||
const structure = await this.createSpatialStructure()
|
||||
return { id: structure.id, tCount: structure.closureLen }
|
||||
}
|
||||
|
||||
async createGeometries() {
|
||||
this.rawGeo = this.api.LoadAllGeometry(this.modelId)
|
||||
async createSpatialStructure() {
|
||||
const chunks = await this.getSpatialTreeChunks()
|
||||
const allProjectLines = await this.ifcapi.GetLineIDsWithType(
|
||||
this.modelId,
|
||||
WebIFC.IFCPROJECT
|
||||
)
|
||||
const project = {
|
||||
expressID: allProjectLines.get(0),
|
||||
type: 'IFCPROJECT',
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Base',
|
||||
elements: []
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.rawGeo.size(); i++) {
|
||||
const mesh = this.rawGeo.get(i)
|
||||
const prodId = mesh.expressID
|
||||
this.productGeo[prodId] = []
|
||||
await this.populateSpatialNode(project, chunks, [], 0)
|
||||
|
||||
for (let j = 0; j < mesh.geometries.size(); j++) {
|
||||
const placedGeom = mesh.geometries.get(j)
|
||||
const geom = this.api.GetGeometry(this.modelId, placedGeom.geometryExpressID)
|
||||
this.endTime = performance.now()
|
||||
project.parseTime = (this.endTime - this.startTime).toFixed(2) + 'ms'
|
||||
project.fileId = this.fileId
|
||||
|
||||
const matrix = placedGeom.flatTransformation
|
||||
const raw = {
|
||||
color: geom.color, // NOTE: material: x, y, z = rgb, w = opacity
|
||||
vertices: this.api.GetVertexArray(
|
||||
geom.GetVertexData(),
|
||||
geom.GetVertexDataSize()
|
||||
),
|
||||
indices: this.api.GetIndexArray(geom.GetIndexData(), geom.GetIndexDataSize())
|
||||
// Last save to db call, empty the last bucket
|
||||
if (this.objectBucket.length !== 0) {
|
||||
await this.flushObjectBucket()
|
||||
}
|
||||
return project
|
||||
}
|
||||
|
||||
async populateSpatialNode(node, chunks, closures, depth) {
|
||||
depth++
|
||||
this.logger.debug(`${this.spatialNodeCount++} nodes generated.`)
|
||||
closures.push([])
|
||||
await this.getChildren(node, chunks, PropNames.aggregates, closures, depth)
|
||||
await this.getChildren(node, chunks, PropNames.spatial, closures, depth)
|
||||
|
||||
node.closure = [...new Set(closures.pop())]
|
||||
|
||||
// get geometry, set displayValue
|
||||
// add geometry ids to closure
|
||||
if (
|
||||
this.geometryReferences[node.expressID] &&
|
||||
this.geometryReferences[node.expressID].length !== 0
|
||||
) {
|
||||
node['@displayValue'] = this.geometryReferences[node.expressID]
|
||||
node.closure.push(
|
||||
...this.geometryReferences[node.expressID].map((ref) => ref.referencedId)
|
||||
)
|
||||
}
|
||||
// node.closureLen = node.closure.length
|
||||
node.__closure = this.formatClosure(node.closure)
|
||||
node.id = getHash(node)
|
||||
|
||||
// Save to db
|
||||
this.objectBucket.push(node)
|
||||
if (this.objectBucket.length > 3000) {
|
||||
await this.flushObjectBucket()
|
||||
}
|
||||
|
||||
// remove project level node closure
|
||||
if (depth === 1) {
|
||||
delete node.closure
|
||||
}
|
||||
return node.id
|
||||
}
|
||||
|
||||
async flushObjectBucket() {
|
||||
if (this.objectBucket.length === 0) return
|
||||
await this.serverApi.saveObjectBatch(this.objectBucket)
|
||||
this.objectBucket = []
|
||||
}
|
||||
|
||||
formatClosure(idsArray) {
|
||||
const cl = {}
|
||||
for (const id of idsArray) cl[id] = 1
|
||||
return cl
|
||||
}
|
||||
|
||||
async getChildren(node, chunks, propName, closures) {
|
||||
const children = chunks[node.expressID]
|
||||
if (!children) return
|
||||
const prop = propName.key
|
||||
const nodes = []
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
let cnode = this.createNode(child)
|
||||
cnode = { ...cnode, ...(await this.getItemProperties(cnode.expressID)) }
|
||||
cnode.id = await this.populateSpatialNode(cnode, chunks, closures)
|
||||
|
||||
for (const closure of closures) {
|
||||
closure.push(cnode.id)
|
||||
if (cnode['closure'].length > 30_000)
|
||||
for (const id of cnode['closure']) closure.push(id)
|
||||
else closure.push(...cnode['closure']) // can stack overflow for large arguments
|
||||
}
|
||||
|
||||
delete cnode.closure
|
||||
nodes.push(cnode)
|
||||
}
|
||||
|
||||
node[prop] = nodes.map((node) => ({
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'reference',
|
||||
referencedId: node.id
|
||||
}))
|
||||
}
|
||||
|
||||
async getItemProperties(id) {
|
||||
if (this.propCache[id]) return this.propCache[id]
|
||||
|
||||
let props = {}
|
||||
const directProps = this.properties[id.toString()]
|
||||
props = { ...directProps }
|
||||
|
||||
const psetIds = []
|
||||
for (let i = 0; i < this.psetRelations.length; i++) {
|
||||
if (this.psetRelations[i].includes(id))
|
||||
psetIds.push(this.psetLines.get(i).toString())
|
||||
}
|
||||
|
||||
const rawPsetIds = psetIds.map((id) =>
|
||||
this.properties[id].RelatingPropertyDefinition.toString()
|
||||
)
|
||||
const rawPsets = rawPsetIds.map((id) => this.properties[id])
|
||||
for (const pset of rawPsets) {
|
||||
props[pset.Name] = this.unpackPsetOrComplexProp(pset)
|
||||
}
|
||||
|
||||
this.propCache[id] = props
|
||||
return props
|
||||
}
|
||||
|
||||
unpackPsetOrComplexProp(pset) {
|
||||
const parsed = {}
|
||||
if (!pset.HasProperties || !Array.isArray(pset.HasProperties)) return parsed
|
||||
for (const id of pset.HasProperties) {
|
||||
const value = this.properties[id.toString()]
|
||||
if (value?.type === 'IFCCOMPLEXPROPERTY') {
|
||||
parsed[value.Name] = this.unpackPsetOrComplexProp(value)
|
||||
} else if (value?.type === 'IFCPROPERTYSINGLEVALUE') {
|
||||
parsed[value.Name] = value.NominalValue
|
||||
}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
async getSpatialTreeChunks() {
|
||||
const treeChunks = {}
|
||||
await this.getChunks(treeChunks, PropNames.aggregates)
|
||||
await this.getChunks(treeChunks, PropNames.spatial)
|
||||
return treeChunks
|
||||
}
|
||||
|
||||
async getChunks(chunks, propName) {
|
||||
const relation = await this.ifcapi.GetLineIDsWithType(this.modelId, propName.name)
|
||||
for (let i = 0; i < relation.size(); i++) {
|
||||
const rel = await this.ifcapi.GetLine(this.modelId, relation.get(i), false)
|
||||
this.saveChunk(chunks, propName, rel)
|
||||
}
|
||||
}
|
||||
|
||||
saveChunk(chunks, propName, rel) {
|
||||
const relating = rel[propName.relating].value
|
||||
const related = rel[propName.related].map((r) => r.value)
|
||||
if (chunks[relating] === undefined) {
|
||||
chunks[relating] = related
|
||||
} else {
|
||||
chunks[relating] = chunks[relating].concat(related)
|
||||
}
|
||||
}
|
||||
|
||||
async getAllTypesOfModel() {
|
||||
const result = {}
|
||||
const elements = Object.keys(IfcElements).map((e) => parseInt(e))
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i]
|
||||
const lines = await this.ifcapi.GetLineIDsWithType(this.modelId, element)
|
||||
const size = lines.size()
|
||||
for (let i = 0; i < size; i++) result[lines.get(i)] = element
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async getAllProps() {
|
||||
const psetLines = this.ifcapi.GetLineIDsWithType(
|
||||
this.modelId,
|
||||
WebIFC.IFCRELDEFINESBYPROPERTIES
|
||||
)
|
||||
const psetRelations = []
|
||||
const properties = {}
|
||||
|
||||
const geometryIds = await this.getAllGeometriesIds()
|
||||
const allLinesIDs = await this.ifcapi.GetAllLines(this.modelId)
|
||||
const allLinesCount = allLinesIDs.size()
|
||||
for (let i = 0; i < allLinesCount; i++) {
|
||||
this.logger.debug(`${((i / allLinesCount) * 100).toFixed(3)}% props.`)
|
||||
const id = allLinesIDs.get(i)
|
||||
if (!geometryIds.has(id)) {
|
||||
const props = await this.getItemProperty(id)
|
||||
if (props) {
|
||||
if (props.type === 'IFCRELDEFINESBYPROPERTIES' && props.RelatedObjects) {
|
||||
psetRelations.push(props.RelatedObjects)
|
||||
}
|
||||
properties[id] = props
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { vertices } = this.extractVertexData(raw.vertices)
|
||||
return { psetLines, psetRelations, properties }
|
||||
}
|
||||
|
||||
for (let k = 0; k < vertices.length; k += 3) {
|
||||
const x = vertices[k],
|
||||
y = vertices[k + 1],
|
||||
z = vertices[k + 2]
|
||||
vertices[k] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]
|
||||
vertices[k + 1] =
|
||||
(matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]) * -1
|
||||
vertices[k + 2] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]
|
||||
}
|
||||
async getItemProperty(id) {
|
||||
try {
|
||||
const props = await this.ifcapi.GetLine(this.modelId, id)
|
||||
if (props.type) {
|
||||
props.type = IfcTypesMap[props.type]
|
||||
}
|
||||
this.inPlaceFormatItemProperties(props)
|
||||
return props
|
||||
} catch (e) {
|
||||
this.logger.error(e, `There was an issue getting props of id ${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Since all faces are triangles, we must add a `0` before each group of 3.
|
||||
const spcklFaces = []
|
||||
for (let i = 0; i < raw.indices.length; i++) {
|
||||
if (i % 3 === 0) spcklFaces.push(0)
|
||||
spcklFaces.push(raw.indices[i])
|
||||
}
|
||||
inPlaceFormatItemProperties(props) {
|
||||
Object.keys(props).forEach((key) => {
|
||||
const value = props[key]
|
||||
if (value && value.value !== undefined) props[key] = value.value
|
||||
else if (Array.isArray(value))
|
||||
props[key] = value.map((item) => {
|
||||
if (item && item.value) return item.value
|
||||
return item
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Create a proper Speckle Mesh
|
||||
const spcklMesh = {
|
||||
createNode(id) {
|
||||
const typeName = this.getNodeType(id)
|
||||
return {
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: typeName,
|
||||
expressID: id,
|
||||
type: typeName,
|
||||
elements: [],
|
||||
properties: null
|
||||
}
|
||||
}
|
||||
|
||||
getNodeType(id) {
|
||||
const typeID = this.types[id]
|
||||
return IfcElements[typeID]
|
||||
}
|
||||
|
||||
async getAllGeometriesIds() {
|
||||
const geometriesIds = new Set()
|
||||
const geomTypesArray = Array.from(GeometryTypes)
|
||||
for (let i = 0; i < geomTypesArray.length; i++) {
|
||||
const category = geomTypesArray[i]
|
||||
const ids = await this.ifcapi.GetLineIDsWithType(this.modelId, category)
|
||||
const idsSize = ids.size()
|
||||
for (let j = 0; j < idsSize; j++) {
|
||||
geometriesIds.add(ids.get(j))
|
||||
}
|
||||
}
|
||||
this.geometryIdsCount = geometriesIds.size
|
||||
return geometriesIds
|
||||
}
|
||||
|
||||
async createAndSaveMeshes() {
|
||||
const geometryReferences = {}
|
||||
let count = 0
|
||||
const speckleMeshes = []
|
||||
|
||||
this.ifcapi.StreamAllMeshes(this.modelId, async (mesh) => {
|
||||
const placedGeometries = mesh.geometries
|
||||
geometryReferences[mesh.expressID] = []
|
||||
for (let i = 0; i < placedGeometries.size(); i++) {
|
||||
const placedGeometry = placedGeometries.get(i)
|
||||
const geometry = this.ifcapi.GetGeometry(
|
||||
this.modelId,
|
||||
placedGeometry.geometryExpressID
|
||||
)
|
||||
|
||||
const verts = [
|
||||
...this.ifcapi.GetVertexArray(
|
||||
geometry.GetVertexData(),
|
||||
geometry.GetVertexDataSize()
|
||||
)
|
||||
]
|
||||
|
||||
const indices = [
|
||||
...this.ifcapi.GetIndexArray(
|
||||
geometry.GetIndexData(),
|
||||
geometry.GetIndexDataSize()
|
||||
)
|
||||
]
|
||||
|
||||
const { vertices } = this.extractVertexData(
|
||||
verts,
|
||||
placedGeometry.flatTransformation
|
||||
)
|
||||
const faces = this.extractFaces(indices)
|
||||
|
||||
const speckleMesh = {
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Objects.Geometry.Mesh',
|
||||
units: 'm',
|
||||
volume: 0,
|
||||
area: 0,
|
||||
faces: spcklFaces,
|
||||
vertices: Array.from(vertices),
|
||||
renderMaterial: placedGeom.color
|
||||
? this.colorToMaterial(placedGeom.color)
|
||||
// random: Math.random(), // TODO: remove, this is here just for performance benchmarking/explicit cache poisoning
|
||||
vertices,
|
||||
faces,
|
||||
renderMaterial: placedGeometry.color
|
||||
? this.colorToMaterial(placedGeometry.color)
|
||||
: null
|
||||
}
|
||||
|
||||
const id = await this.serverApi.saveObject(spcklMesh)
|
||||
const ref = { speckle_type: 'reference', referencedId: id }
|
||||
this.productGeo[prodId].push(ref)
|
||||
speckleMesh.id = getHash(speckleMesh)
|
||||
// Note: the web-ifc api disposes of the data post callback, and doesn't know that it's async;
|
||||
// we cannot and should not await things in here. I'm not entirely sure what's going on :)
|
||||
// await this.serverApi.saveObject(speckleMesh)
|
||||
|
||||
speckleMeshes.push(speckleMesh)
|
||||
geometryReferences[mesh.expressID].push({
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'reference',
|
||||
referencedId: speckleMesh.id
|
||||
})
|
||||
this.logger.debug(`${(count++).toFixed(3)} geoms generated.`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await this.serverApi.saveObjectBatch(speckleMeshes)
|
||||
return geometryReferences
|
||||
}
|
||||
|
||||
async traverse(
|
||||
element,
|
||||
recursive = true,
|
||||
depth = 0,
|
||||
specialTypes = [
|
||||
{ type: 'IfcProject', key: 'Name' },
|
||||
{ type: 'IfcBuilding', key: 'Name' },
|
||||
{ type: 'IfcSite', key: 'Name' }
|
||||
]
|
||||
) {
|
||||
// Fast exit if null/undefined
|
||||
if (!element) return
|
||||
|
||||
// If array, traverse all items in it.
|
||||
if (Array.isArray(element)) {
|
||||
return await Promise.all(
|
||||
element.map(
|
||||
async (el) => await this.traverse(el, recursive, depth + 1, specialTypes)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// If it has no expressID, its either a simple type or a { type, value } object.
|
||||
if (!element.expressID) {
|
||||
return await Promise.resolve(
|
||||
element.value !== null && element.value !== undefined ? element.value : element
|
||||
)
|
||||
}
|
||||
|
||||
if (this.cache[element.expressID.toString()])
|
||||
return this.cache[element.expressID.toString()]
|
||||
// If you got here -> It's an IFC Element: create base object, upload and return ref.
|
||||
// logger.debug( `Traversing element ${element.expressID}; Recurse: ${recursive}; Stack ${depth}` )
|
||||
|
||||
// Traverse all key/value pairs first.
|
||||
for (const key of Object.keys(element)) {
|
||||
element[key] = await this.traverse(
|
||||
element[key],
|
||||
recursive,
|
||||
depth + 1,
|
||||
specialTypes
|
||||
)
|
||||
}
|
||||
|
||||
// Assign speckle_type and empty closure table.
|
||||
element.speckle_type = element.constructor.name
|
||||
element.__closure = {}
|
||||
|
||||
// Find spatial children and assign to element
|
||||
const spatialChildrenIds = this.getAllRelatedItemsOfType(
|
||||
element.expressID,
|
||||
WebIFC.IFCRELAGGREGATES,
|
||||
'RelatingObject',
|
||||
'RelatedObjects'
|
||||
)
|
||||
if (spatialChildrenIds.length > 0)
|
||||
element.rawSpatialChildren = spatialChildrenIds.map((childId) =>
|
||||
this.api.GetLine(this.modelId, childId, true)
|
||||
)
|
||||
|
||||
// Find children and populate element
|
||||
const childrenIds = this.getAllRelatedItemsOfType(
|
||||
element.expressID,
|
||||
WebIFC.IFCRELCONTAINEDINSPATIALSTRUCTURE,
|
||||
'RelatingStructure',
|
||||
'RelatedElements'
|
||||
)
|
||||
if (childrenIds.length > 0)
|
||||
element.rawChildren = childrenIds.map((childId) =>
|
||||
this.api.GetLine(this.modelId, childId, true)
|
||||
)
|
||||
|
||||
// Find related property sets
|
||||
const psetsIds = this.getAllRelatedItemsOfType(
|
||||
element.expressID,
|
||||
WebIFC.IFCRELDEFINESBYPROPERTIES,
|
||||
'RelatingPropertyDefinition',
|
||||
'RelatedObjects'
|
||||
)
|
||||
if (psetsIds.length > 0)
|
||||
element.rawPsets = psetsIds.map((childId) =>
|
||||
this.api.GetLine(this.modelId, childId, true)
|
||||
)
|
||||
|
||||
// Find related type properties
|
||||
const typePropsId = this.getAllRelatedItemsOfType(
|
||||
element.expressID,
|
||||
WebIFC.IFCRELDEFINESBYTYPE,
|
||||
'RelatingType',
|
||||
'RelatedObjects'
|
||||
)
|
||||
if (typePropsId.length > 0)
|
||||
element.rawTypeProps = typePropsId.map((childId) =>
|
||||
this.api.GetLine(this.modelId, childId, true)
|
||||
)
|
||||
|
||||
// Lookup geometry in generated geometries object
|
||||
if (this.productGeo[element.expressID]) {
|
||||
element['@displayValue'] = this.productGeo[element.expressID]
|
||||
this.productGeo[element.expressID].forEach((ref) => {
|
||||
this.project.__closure[ref.referencedId.toString()] = depth
|
||||
element.__closure[ref.referencedId.toString()] = 1
|
||||
})
|
||||
}
|
||||
|
||||
const isSpecial = specialTypes.find((t) => t.type === element.speckle_type)
|
||||
// Recurse all children
|
||||
if (recursive) {
|
||||
await this.processSubElements(
|
||||
element,
|
||||
'rawSpatialChildren',
|
||||
'spatialChildren',
|
||||
isSpecial,
|
||||
recursive,
|
||||
depth,
|
||||
specialTypes
|
||||
)
|
||||
await this.processSubElements(
|
||||
element,
|
||||
'rawChildren',
|
||||
'children',
|
||||
isSpecial,
|
||||
recursive,
|
||||
depth,
|
||||
specialTypes
|
||||
)
|
||||
await this.processSubElements(
|
||||
element,
|
||||
'rawPsets',
|
||||
'propertySets',
|
||||
false,
|
||||
recursive,
|
||||
depth,
|
||||
specialTypes
|
||||
)
|
||||
await this.processSubElements(
|
||||
element,
|
||||
'rawTypeProps',
|
||||
'typeProps',
|
||||
false,
|
||||
recursive,
|
||||
depth,
|
||||
specialTypes
|
||||
)
|
||||
|
||||
if (
|
||||
element.children ||
|
||||
element.spatialChildren ||
|
||||
element.propertySets ||
|
||||
element.typeProps
|
||||
) {
|
||||
logger.info(
|
||||
`${element.constructor.name} ${element.GlobalId}:\n\tchildren count: ${
|
||||
element.children ? element.children.length : '0'
|
||||
};\n\tspatial children count: ${
|
||||
element.spatialChildren ? element.spatialChildren.length : '0'
|
||||
};\n\tproperty sets count: ${
|
||||
element.propertySets ? element.propertySets.length : 0
|
||||
};\n\ttype properties: ${element.typeProps ? element.typeProps.length : 0}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.productGeo[element.expressID] ||
|
||||
element.spatialChildren ||
|
||||
element.children
|
||||
) {
|
||||
const id = await this.serverApi.saveObject(element)
|
||||
const ref = { speckle_type: 'reference', referencedId: id }
|
||||
this.cache[element.expressID.toString()] = ref
|
||||
this.closureCache[element.expressID.toString()] = element.__closure
|
||||
return ref
|
||||
} else {
|
||||
this.cache[element.expressID.toString()] = element
|
||||
this.closureCache[element.expressID.toString()] = element.__closure
|
||||
return element
|
||||
extractFaces(indices) {
|
||||
const faces = []
|
||||
for (let i = 0; i < indices.length; i++) {
|
||||
if (i % 3 === 0) faces.push(0)
|
||||
faces.push(indices[i])
|
||||
}
|
||||
return faces
|
||||
}
|
||||
|
||||
async processSubElements(
|
||||
element,
|
||||
key,
|
||||
newKey,
|
||||
isSpecial,
|
||||
recursive,
|
||||
depth,
|
||||
specialTypes
|
||||
) {
|
||||
if (element[key]) {
|
||||
if (!isSpecial) element[newKey] = []
|
||||
const childCount = {}
|
||||
for (const child of element[key]) {
|
||||
const res = await this.traverse(child, recursive, depth + 1, specialTypes)
|
||||
if (res.referencedId) {
|
||||
if (isSpecial) {
|
||||
let name = child[isSpecial.key]
|
||||
if (!name || name.length === 0) name = 'Undefined'
|
||||
if (!childCount[name]) childCount[name] = 0
|
||||
if (childCount[name] > 0) name += '-' + childCount[name]++
|
||||
element[name] = res
|
||||
} else element[newKey].push(res)
|
||||
this.project.__closure[res.referencedId.toString()] = depth
|
||||
element.__closure[res.referencedId.toString()] = 1
|
||||
|
||||
// adds to parent (this element) the child's closure tree.
|
||||
if (this.closureCache[child.expressID.toString()]) {
|
||||
for (const key of Object.keys(
|
||||
this.closureCache[child.expressID.toString()]
|
||||
)) {
|
||||
element.__closure[key] =
|
||||
this.closureCache[child.expressID.toString()][key] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete element[key]
|
||||
}
|
||||
}
|
||||
|
||||
// (c) https://github.com/agviegas/web-ifc-three
|
||||
extractVertexData(vertexData) {
|
||||
extractVertexData(vertexData, matrix) {
|
||||
const vertices = []
|
||||
const normals = []
|
||||
let isNormalData = false
|
||||
|
@ -330,45 +405,37 @@ module.exports = class IFCParser {
|
|||
isNormalData ? normals.push(vertexData[i]) : vertices.push(vertexData[i])
|
||||
if ((i + 1) % 3 === 0) isNormalData = !isNormalData
|
||||
}
|
||||
|
||||
// apply the transform
|
||||
for (let k = 0; k < vertices.length; k += 3) {
|
||||
const x = vertices[k],
|
||||
y = vertices[k + 1],
|
||||
z = vertices[k + 2]
|
||||
vertices[k] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]
|
||||
vertices[k + 1] =
|
||||
(matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]) * -1
|
||||
vertices[k + 2] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]
|
||||
}
|
||||
|
||||
return { vertices, normals }
|
||||
}
|
||||
|
||||
// (c) https://github.com/agviegas/web-ifc-three/blob/907e08b5673d5e1c18261a4fceade7189d6b2db7/src/IFC/PropertyManager.ts#L110
|
||||
getAllRelatedItemsOfType(elementID, type, relation, relatedProperty) {
|
||||
const lines = this.api.GetLineIDsWithType(this.modelId, type)
|
||||
const IDs = []
|
||||
|
||||
for (let i = 0; i < lines.size(); i++) {
|
||||
const relID = lines.get(i)
|
||||
const rel = this.api.GetLine(this.modelId, relID)
|
||||
const relatedItems = rel[relation]
|
||||
let foundElement = false
|
||||
|
||||
if (Array.isArray(relatedItems)) {
|
||||
const values = relatedItems.map((item) => item.value)
|
||||
foundElement = values.includes(elementID)
|
||||
} else foundElement = relatedItems.value === elementID
|
||||
|
||||
if (foundElement) {
|
||||
const element = rel[relatedProperty]
|
||||
if (!Array.isArray(element)) IDs.push(element.value)
|
||||
else element.forEach((ele) => IDs.push(ele.value))
|
||||
}
|
||||
}
|
||||
|
||||
return IDs
|
||||
}
|
||||
|
||||
colorToMaterial(color) {
|
||||
const intColor =
|
||||
(color.w << 24) + ((color.x * 255) << 16) + ((color.y * 255) << 8) + color.z * 255
|
||||
|
||||
return {
|
||||
const intColor = Math.floor(
|
||||
((color.w * 255) << 24) +
|
||||
((color.x * 255) << 16) +
|
||||
((color.y * 255) << 8) +
|
||||
color.z * 255
|
||||
)
|
||||
const material = {
|
||||
diffuse: intColor,
|
||||
opacity: color.w,
|
||||
metalness: 0,
|
||||
roughness: 1,
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Objects.Other.RenderMaterial'
|
||||
}
|
||||
material.id = getHash(material)
|
||||
return material
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,441 +0,0 @@
|
|||
const { performance } = require('perf_hooks')
|
||||
const WebIFC = require('web-ifc/web-ifc-api-node')
|
||||
const {
|
||||
getHash,
|
||||
IfcElements,
|
||||
PropNames,
|
||||
GeometryTypes,
|
||||
IfcTypesMap
|
||||
} = require('./utils')
|
||||
const Observability = require('@speckle/shared/dist/commonjs/observability/index.js')
|
||||
const { logger: parentLogger } = require('../observability/logging')
|
||||
|
||||
module.exports = class IFCParser {
|
||||
constructor({ serverApi, fileId, logger }) {
|
||||
this.ifcapi = new WebIFC.IfcAPI()
|
||||
this.ifcapi.SetWasmPath('./', false)
|
||||
this.serverApi = serverApi
|
||||
this.fileId = fileId
|
||||
this.logger =
|
||||
logger ||
|
||||
Observability.extendLoggerComponent(parentLogger.child({ fileId }), 'ifc')
|
||||
}
|
||||
|
||||
async parse(data) {
|
||||
await this.ifcapi.Init()
|
||||
this.modelId = this.ifcapi.OpenModel(new Uint8Array(data), {
|
||||
USE_FAST_BOOLS: true
|
||||
})
|
||||
|
||||
this.startTime = performance.now()
|
||||
|
||||
// prepoulate types
|
||||
this.types = await this.getAllTypesOfModel()
|
||||
|
||||
// prime caches for property sets and their relating objects, as well as,
|
||||
// most importantly, all the properties.
|
||||
const { psetLines, psetRelations, properties } = await this.getAllProps()
|
||||
this.psetLines = psetLines
|
||||
this.psetRelations = psetRelations
|
||||
this.properties = properties
|
||||
|
||||
this.propCache = {}
|
||||
|
||||
// This is used to pre-batch ifc objects that need to be persisted.
|
||||
this.objectBucket = []
|
||||
|
||||
// create and save the geometries; we're storing only references locally.
|
||||
this.geometryReferences = await this.createAndSaveMeshes()
|
||||
|
||||
// create and save the spatial tree, populating both properties and geometry references
|
||||
// where appropriate
|
||||
this.spatialNodeCount = 0
|
||||
const structure = await this.createSpatialStructure()
|
||||
return { id: structure.id, tCount: structure.closureLen }
|
||||
}
|
||||
|
||||
async createSpatialStructure() {
|
||||
const chunks = await this.getSpatialTreeChunks()
|
||||
const allProjectLines = await this.ifcapi.GetLineIDsWithType(
|
||||
this.modelId,
|
||||
WebIFC.IFCPROJECT
|
||||
)
|
||||
const project = {
|
||||
expressID: allProjectLines.get(0),
|
||||
type: 'IFCPROJECT',
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Base',
|
||||
elements: []
|
||||
}
|
||||
|
||||
await this.populateSpatialNode(project, chunks, [], 0)
|
||||
|
||||
this.endTime = performance.now()
|
||||
project.parseTime = (this.endTime - this.startTime).toFixed(2) + 'ms'
|
||||
project.fileId = this.fileId
|
||||
|
||||
// Last save to db call, empty the last bucket
|
||||
if (this.objectBucket.length !== 0) {
|
||||
await this.flushObjectBucket()
|
||||
}
|
||||
return project
|
||||
}
|
||||
|
||||
async populateSpatialNode(node, chunks, closures, depth) {
|
||||
depth++
|
||||
this.logger.debug(`${this.spatialNodeCount++} nodes generated.`)
|
||||
closures.push([])
|
||||
await this.getChildren(node, chunks, PropNames.aggregates, closures, depth)
|
||||
await this.getChildren(node, chunks, PropNames.spatial, closures, depth)
|
||||
|
||||
node.closure = [...new Set(closures.pop())]
|
||||
|
||||
// get geometry, set displayValue
|
||||
// add geometry ids to closure
|
||||
if (
|
||||
this.geometryReferences[node.expressID] &&
|
||||
this.geometryReferences[node.expressID].length !== 0
|
||||
) {
|
||||
node['@displayValue'] = this.geometryReferences[node.expressID]
|
||||
node.closure.push(
|
||||
...this.geometryReferences[node.expressID].map((ref) => ref.referencedId)
|
||||
)
|
||||
}
|
||||
// node.closureLen = node.closure.length
|
||||
node.__closure = this.formatClosure(node.closure)
|
||||
node.id = getHash(node)
|
||||
|
||||
// Save to db
|
||||
this.objectBucket.push(node)
|
||||
if (this.objectBucket.length > 3000) {
|
||||
await this.flushObjectBucket()
|
||||
}
|
||||
|
||||
// remove project level node closure
|
||||
if (depth === 1) {
|
||||
delete node.closure
|
||||
}
|
||||
return node.id
|
||||
}
|
||||
|
||||
async flushObjectBucket() {
|
||||
if (this.objectBucket.length === 0) return
|
||||
await this.serverApi.saveObjectBatch(this.objectBucket)
|
||||
this.objectBucket = []
|
||||
}
|
||||
|
||||
formatClosure(idsArray) {
|
||||
const cl = {}
|
||||
for (const id of idsArray) cl[id] = 1
|
||||
return cl
|
||||
}
|
||||
|
||||
async getChildren(node, chunks, propName, closures) {
|
||||
const children = chunks[node.expressID]
|
||||
if (!children) return
|
||||
const prop = propName.key
|
||||
const nodes = []
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
let cnode = this.createNode(child)
|
||||
cnode = { ...cnode, ...(await this.getItemProperties(cnode.expressID)) }
|
||||
cnode.id = await this.populateSpatialNode(cnode, chunks, closures)
|
||||
|
||||
for (const closure of closures) {
|
||||
closure.push(cnode.id)
|
||||
if (cnode['closure'].length > 30_000)
|
||||
for (const id of cnode['closure']) closure.push(id)
|
||||
else closure.push(...cnode['closure']) // can stack overflow for large arguments
|
||||
}
|
||||
|
||||
delete cnode.closure
|
||||
nodes.push(cnode)
|
||||
}
|
||||
|
||||
node[prop] = nodes.map((node) => ({
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'reference',
|
||||
referencedId: node.id
|
||||
}))
|
||||
}
|
||||
|
||||
async getItemProperties(id) {
|
||||
if (this.propCache[id]) return this.propCache[id]
|
||||
|
||||
let props = {}
|
||||
const directProps = this.properties[id.toString()]
|
||||
props = { ...directProps }
|
||||
|
||||
const psetIds = []
|
||||
for (let i = 0; i < this.psetRelations.length; i++) {
|
||||
if (this.psetRelations[i].includes(id))
|
||||
psetIds.push(this.psetLines.get(i).toString())
|
||||
}
|
||||
|
||||
const rawPsetIds = psetIds.map((id) =>
|
||||
this.properties[id].RelatingPropertyDefinition.toString()
|
||||
)
|
||||
const rawPsets = rawPsetIds.map((id) => this.properties[id])
|
||||
for (const pset of rawPsets) {
|
||||
props[pset.Name] = this.unpackPsetOrComplexProp(pset)
|
||||
}
|
||||
|
||||
this.propCache[id] = props
|
||||
return props
|
||||
}
|
||||
|
||||
unpackPsetOrComplexProp(pset) {
|
||||
const parsed = {}
|
||||
if (!pset.HasProperties || !Array.isArray(pset.HasProperties)) return parsed
|
||||
for (const id of pset.HasProperties) {
|
||||
const value = this.properties[id.toString()]
|
||||
if (value?.type === 'IFCCOMPLEXPROPERTY') {
|
||||
parsed[value.Name] = this.unpackPsetOrComplexProp(value)
|
||||
} else if (value?.type === 'IFCPROPERTYSINGLEVALUE') {
|
||||
parsed[value.Name] = value.NominalValue
|
||||
}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
async getSpatialTreeChunks() {
|
||||
const treeChunks = {}
|
||||
await this.getChunks(treeChunks, PropNames.aggregates)
|
||||
await this.getChunks(treeChunks, PropNames.spatial)
|
||||
return treeChunks
|
||||
}
|
||||
|
||||
async getChunks(chunks, propName) {
|
||||
const relation = await this.ifcapi.GetLineIDsWithType(this.modelId, propName.name)
|
||||
for (let i = 0; i < relation.size(); i++) {
|
||||
const rel = await this.ifcapi.GetLine(this.modelId, relation.get(i), false)
|
||||
this.saveChunk(chunks, propName, rel)
|
||||
}
|
||||
}
|
||||
|
||||
saveChunk(chunks, propName, rel) {
|
||||
const relating = rel[propName.relating].value
|
||||
const related = rel[propName.related].map((r) => r.value)
|
||||
if (chunks[relating] === undefined) {
|
||||
chunks[relating] = related
|
||||
} else {
|
||||
chunks[relating] = chunks[relating].concat(related)
|
||||
}
|
||||
}
|
||||
|
||||
async getAllTypesOfModel() {
|
||||
const result = {}
|
||||
const elements = Object.keys(IfcElements).map((e) => parseInt(e))
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i]
|
||||
const lines = await this.ifcapi.GetLineIDsWithType(this.modelId, element)
|
||||
const size = lines.size()
|
||||
for (let i = 0; i < size; i++) result[lines.get(i)] = element
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async getAllProps() {
|
||||
const psetLines = this.ifcapi.GetLineIDsWithType(
|
||||
this.modelId,
|
||||
WebIFC.IFCRELDEFINESBYPROPERTIES
|
||||
)
|
||||
const psetRelations = []
|
||||
const properties = {}
|
||||
|
||||
const geometryIds = await this.getAllGeometriesIds()
|
||||
const allLinesIDs = await this.ifcapi.GetAllLines(this.modelId)
|
||||
const allLinesCount = allLinesIDs.size()
|
||||
for (let i = 0; i < allLinesCount; i++) {
|
||||
this.logger.debug(`${((i / allLinesCount) * 100).toFixed(3)}% props.`)
|
||||
const id = allLinesIDs.get(i)
|
||||
if (!geometryIds.has(id)) {
|
||||
const props = await this.getItemProperty(id)
|
||||
if (props) {
|
||||
if (props.type === 'IFCRELDEFINESBYPROPERTIES' && props.RelatedObjects) {
|
||||
psetRelations.push(props.RelatedObjects)
|
||||
}
|
||||
properties[id] = props
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { psetLines, psetRelations, properties }
|
||||
}
|
||||
|
||||
async getItemProperty(id) {
|
||||
try {
|
||||
const props = await this.ifcapi.GetLine(this.modelId, id)
|
||||
if (props.type) {
|
||||
props.type = IfcTypesMap[props.type]
|
||||
}
|
||||
this.inPlaceFormatItemProperties(props)
|
||||
return props
|
||||
} catch (e) {
|
||||
this.logger.error(e, `There was an issue getting props of id ${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
inPlaceFormatItemProperties(props) {
|
||||
Object.keys(props).forEach((key) => {
|
||||
const value = props[key]
|
||||
if (value && value.value !== undefined) props[key] = value.value
|
||||
else if (Array.isArray(value))
|
||||
props[key] = value.map((item) => {
|
||||
if (item && item.value) return item.value
|
||||
return item
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createNode(id) {
|
||||
const typeName = this.getNodeType(id)
|
||||
return {
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: typeName,
|
||||
expressID: id,
|
||||
type: typeName,
|
||||
elements: [],
|
||||
properties: null
|
||||
}
|
||||
}
|
||||
|
||||
getNodeType(id) {
|
||||
const typeID = this.types[id]
|
||||
return IfcElements[typeID]
|
||||
}
|
||||
|
||||
async getAllGeometriesIds() {
|
||||
const geometriesIds = new Set()
|
||||
const geomTypesArray = Array.from(GeometryTypes)
|
||||
for (let i = 0; i < geomTypesArray.length; i++) {
|
||||
const category = geomTypesArray[i]
|
||||
const ids = await this.ifcapi.GetLineIDsWithType(this.modelId, category)
|
||||
const idsSize = ids.size()
|
||||
for (let j = 0; j < idsSize; j++) {
|
||||
geometriesIds.add(ids.get(j))
|
||||
}
|
||||
}
|
||||
this.geometryIdsCount = geometriesIds.size
|
||||
return geometriesIds
|
||||
}
|
||||
|
||||
async createAndSaveMeshes() {
|
||||
const geometryReferences = {}
|
||||
let count = 0
|
||||
const speckleMeshes = []
|
||||
|
||||
this.ifcapi.StreamAllMeshes(this.modelId, async (mesh) => {
|
||||
const placedGeometries = mesh.geometries
|
||||
geometryReferences[mesh.expressID] = []
|
||||
for (let i = 0; i < placedGeometries.size(); i++) {
|
||||
const placedGeometry = placedGeometries.get(i)
|
||||
const geometry = this.ifcapi.GetGeometry(
|
||||
this.modelId,
|
||||
placedGeometry.geometryExpressID
|
||||
)
|
||||
|
||||
const verts = [
|
||||
...this.ifcapi.GetVertexArray(
|
||||
geometry.GetVertexData(),
|
||||
geometry.GetVertexDataSize()
|
||||
)
|
||||
]
|
||||
|
||||
const indices = [
|
||||
...this.ifcapi.GetIndexArray(
|
||||
geometry.GetIndexData(),
|
||||
geometry.GetIndexDataSize()
|
||||
)
|
||||
]
|
||||
|
||||
const { vertices } = this.extractVertexData(
|
||||
verts,
|
||||
placedGeometry.flatTransformation
|
||||
)
|
||||
const faces = this.extractFaces(indices)
|
||||
|
||||
const speckleMesh = {
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Objects.Geometry.Mesh',
|
||||
units: 'm',
|
||||
volume: 0,
|
||||
area: 0,
|
||||
// random: Math.random(), // TODO: remove, this is here just for performance benchmarking/explicit cache poisoning
|
||||
vertices,
|
||||
faces,
|
||||
renderMaterial: placedGeometry.color
|
||||
? this.colorToMaterial(placedGeometry.color)
|
||||
: null
|
||||
}
|
||||
|
||||
speckleMesh.id = getHash(speckleMesh)
|
||||
// Note: the web-ifc api disposes of the data post callback, and doesn't know that it's async;
|
||||
// we cannot and should not await things in here. I'm not entirely sure what's going on :)
|
||||
// await this.serverApi.saveObject(speckleMesh)
|
||||
|
||||
speckleMeshes.push(speckleMesh)
|
||||
geometryReferences[mesh.expressID].push({
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'reference',
|
||||
referencedId: speckleMesh.id
|
||||
})
|
||||
this.logger.debug(`${(count++).toFixed(3)} geoms generated.`)
|
||||
}
|
||||
})
|
||||
|
||||
await this.serverApi.saveObjectBatch(speckleMeshes)
|
||||
return geometryReferences
|
||||
}
|
||||
|
||||
extractFaces(indices) {
|
||||
const faces = []
|
||||
for (let i = 0; i < indices.length; i++) {
|
||||
if (i % 3 === 0) faces.push(0)
|
||||
faces.push(indices[i])
|
||||
}
|
||||
return faces
|
||||
}
|
||||
|
||||
extractVertexData(vertexData, matrix) {
|
||||
const vertices = []
|
||||
const normals = []
|
||||
let isNormalData = false
|
||||
for (let i = 0; i < vertexData.length; i++) {
|
||||
isNormalData ? normals.push(vertexData[i]) : vertices.push(vertexData[i])
|
||||
if ((i + 1) % 3 === 0) isNormalData = !isNormalData
|
||||
}
|
||||
|
||||
// apply the transform
|
||||
for (let k = 0; k < vertices.length; k += 3) {
|
||||
const x = vertices[k],
|
||||
y = vertices[k + 1],
|
||||
z = vertices[k + 2]
|
||||
vertices[k] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]
|
||||
vertices[k + 1] =
|
||||
(matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]) * -1
|
||||
vertices[k + 2] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]
|
||||
}
|
||||
|
||||
return { vertices, normals }
|
||||
}
|
||||
|
||||
colorToMaterial(color) {
|
||||
const intColor = Math.floor(
|
||||
((color.w * 255) << 24) +
|
||||
((color.x * 255) << 16) +
|
||||
((color.y * 255) << 8) +
|
||||
color.z * 255
|
||||
)
|
||||
const material = {
|
||||
diffuse: intColor,
|
||||
opacity: color.w,
|
||||
metalness: 0,
|
||||
roughness: 1,
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Objects.Other.RenderMaterial'
|
||||
}
|
||||
material.id = getHash(material)
|
||||
return material
|
||||
}
|
||||
}
|
|
@ -1,18 +1,52 @@
|
|||
/* eslint-disable camelcase */
|
||||
'use strict'
|
||||
|
||||
module.exports = require('knex')({
|
||||
client: 'pg',
|
||||
connection: {
|
||||
application_name: 'speckle_fileimport_service',
|
||||
connectionString:
|
||||
process.env.PG_CONNECTION_STRING || 'postgres://speckle:speckle@127.0.0.1/speckle'
|
||||
},
|
||||
pool: {
|
||||
min: 0,
|
||||
max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS_FILE_IMPORT_SERVICE) || 1,
|
||||
acquireTimeoutMillis: 16000, //allows for 3x creation attempts plus idle time between attempts
|
||||
createTimeoutMillis: 5000
|
||||
const Environment = require('@speckle/shared/dist/commonjs/environment/index.js')
|
||||
const {
|
||||
loadMultiRegionsConfig,
|
||||
configureKnexClient
|
||||
} = require('@speckle/shared/dist/commonjs/environment/multiRegionConfig.js')
|
||||
const { logger } = require('./observability/logging')
|
||||
|
||||
const { FF_WORKSPACES_MULTI_REGION_ENABLED } = Environment.getFeatureFlags()
|
||||
|
||||
const isDevEnv = process.env.NODE_ENV !== 'production'
|
||||
|
||||
let dbClients
|
||||
const getDbClients = async () => {
|
||||
if (dbClients) return dbClients
|
||||
const maxConnections =
|
||||
parseInt(process.env.POSTGRES_MAX_CONNECTIONS_FILE_IMPORT_SERVICE) || 1
|
||||
|
||||
const configArgs = {
|
||||
migrationDirs: [],
|
||||
isTestEnv: isDevEnv,
|
||||
isDevOrTestEnv: isDevEnv,
|
||||
logger,
|
||||
maxConnections,
|
||||
applicationName: 'speckle_fileimport_service'
|
||||
}
|
||||
// migrations are in managed in the server package
|
||||
})
|
||||
if (!FF_WORKSPACES_MULTI_REGION_ENABLED) {
|
||||
const mainClient = configureKnexClient(
|
||||
{
|
||||
postgres: {
|
||||
connectionUri:
|
||||
process.env.PG_CONNECTION_STRING ||
|
||||
'postgres://speckle:speckle@127.0.0.1/speckle'
|
||||
}
|
||||
},
|
||||
configArgs
|
||||
)
|
||||
dbClients = { main: mainClient }
|
||||
} else {
|
||||
const configPath = process.env.MULTI_REGION_CONFIG_PATH || 'multiregion.json'
|
||||
const config = await loadMultiRegionsConfig({ path: configPath })
|
||||
const clients = [['main', configureKnexClient(config.main, configArgs)]]
|
||||
Object.entries(config.regions).map(([key, config]) => {
|
||||
clients.push([key, configureKnexClient(config, configArgs)])
|
||||
})
|
||||
dbClients = Object.fromEntries(clients)
|
||||
}
|
||||
return dbClients
|
||||
}
|
||||
|
||||
module.exports = getDbClients
|
||||
|
|
|
@ -159,6 +159,8 @@ if __name__ == "__main__":
|
|||
commit_id = import_obj()
|
||||
if not commit_id:
|
||||
raise Exception("Can't create commit")
|
||||
if isinstance(commit_id, Exception):
|
||||
raise commit_id
|
||||
results = {"success": True, "commitId": commit_id}
|
||||
except Exception as ex:
|
||||
LOG.exception(ex)
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
"private": true,
|
||||
"version": "2.5.4",
|
||||
"description": "Parse and import files of various types into a stream",
|
||||
"author": "Dimitrie Stefanescu <didimitrie@gmail.com>",
|
||||
"author": "Speckle Systems <hello@speckle.systems>",
|
||||
"homepage": "https://github.com/specklesystems/speckle-server#readme",
|
||||
"license": "SEE LICENSE IN readme.md",
|
||||
"main": "index.js",
|
||||
"main": "daemon.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/specklesystems/speckle-server.git"
|
||||
|
@ -34,7 +34,9 @@
|
|||
"prom-client": "^14.0.1",
|
||||
"undici": "^5.28.4",
|
||||
"valid-filename": "^3.1.0",
|
||||
"web-ifc": "^0.0.36"
|
||||
"web-ifc": "^0.0.36",
|
||||
"znv": "^0.4.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
|
|
|
@ -6,8 +6,7 @@ const {
|
|||
metricInputFileSize,
|
||||
metricOperationErrors
|
||||
} = require('./prometheusMetrics')
|
||||
const knex = require('../knex')
|
||||
const FileUploads = () => knex('file_uploads')
|
||||
const getDbClients = require('../knex')
|
||||
|
||||
const { downloadFile } = require('./filesApi')
|
||||
const fs = require('fs')
|
||||
|
@ -16,7 +15,7 @@ const { spawn } = require('child_process')
|
|||
const ServerAPI = require('../ifc/api')
|
||||
const objDependencies = require('./objDependencies')
|
||||
const { logger } = require('../observability/logging')
|
||||
const { Scopes } = require('@speckle/shared')
|
||||
const { Scopes, wait } = require('@speckle/shared')
|
||||
|
||||
const HEALTHCHECK_FILE_PATH = '/tmp/last_successful_query'
|
||||
|
||||
|
@ -31,10 +30,10 @@ let TIME_LIMIT = 10 * 60 * 1000
|
|||
const providedTimeLimit = parseInt(process.env.FILE_IMPORT_TIME_LIMIT_MIN)
|
||||
if (providedTimeLimit) TIME_LIMIT = providedTimeLimit * 60 * 1000
|
||||
|
||||
async function startTask() {
|
||||
async function startTask(knex) {
|
||||
const { rows } = await knex.raw(`
|
||||
UPDATE file_uploads
|
||||
SET
|
||||
SET
|
||||
"convertedStatus" = 1,
|
||||
"convertedLastUpdate" = NOW()
|
||||
FROM (
|
||||
|
@ -49,15 +48,16 @@ async function startTask() {
|
|||
return rows[0]
|
||||
}
|
||||
|
||||
async function doTask(task) {
|
||||
async function doTask(mainDb, regionName, taskDb, task) {
|
||||
const taskId = task.id
|
||||
|
||||
// Mark task as started
|
||||
await knex.raw(`NOTIFY file_import_started, '${task.id}'`)
|
||||
await mainDb.raw(`NOTIFY file_import_started, '${task.id}'`)
|
||||
|
||||
let taskLogger = logger.child({ taskId })
|
||||
let tempUserToken = null
|
||||
let serverApi = null
|
||||
let mainServerApi = null
|
||||
let taskServerApi = null
|
||||
let fileTypeForMetric = 'unknown'
|
||||
let fileSizeForMetric = 0
|
||||
|
||||
|
@ -67,7 +67,7 @@ async function doTask(task) {
|
|||
|
||||
try {
|
||||
taskLogger.info("Doing task '{taskId}'.")
|
||||
const info = await FileUploads().where({ id: taskId }).first()
|
||||
const info = await taskDb('file_uploads').where({ id: taskId }).first()
|
||||
if (!info) {
|
||||
throw new Error('Internal error: DB inconsistent')
|
||||
}
|
||||
|
@ -85,13 +85,22 @@ async function doTask(task) {
|
|||
})
|
||||
fs.mkdirSync(TMP_INPUT_DIR, { recursive: true })
|
||||
|
||||
serverApi = new ServerAPI({ streamId: info.streamId, logger: taskLogger })
|
||||
mainServerApi = new ServerAPI({
|
||||
db: mainDb,
|
||||
streamId: info.streamId,
|
||||
logger: taskLogger
|
||||
})
|
||||
taskServerApi = new ServerAPI({
|
||||
db: taskDb,
|
||||
streamId: info.streamId,
|
||||
logger: taskLogger
|
||||
})
|
||||
|
||||
branchMetadata = {
|
||||
branchName: info.branchName,
|
||||
streamId: info.streamId
|
||||
}
|
||||
const existingBranch = await serverApi.getBranchByNameAndStreamId({
|
||||
const existingBranch = await taskServerApi.getBranchByNameAndStreamId({
|
||||
streamId: info.streamId,
|
||||
name: info.branchName
|
||||
})
|
||||
|
@ -99,7 +108,7 @@ async function doTask(task) {
|
|||
newBranchCreated = true
|
||||
}
|
||||
|
||||
const { token } = await serverApi.createToken({
|
||||
const { token } = await mainServerApi.createToken({
|
||||
userId: info.userId,
|
||||
name: 'temp upload token',
|
||||
scopes: [Scopes.Streams.Write, Scopes.Streams.Read],
|
||||
|
@ -126,7 +135,8 @@ async function doTask(task) {
|
|||
info.streamId,
|
||||
info.branchName,
|
||||
`File upload: ${info.fileName}`,
|
||||
info.id
|
||||
info.id,
|
||||
regionName
|
||||
],
|
||||
{
|
||||
USER_TOKEN: tempUserToken
|
||||
|
@ -185,7 +195,7 @@ async function doTask(task) {
|
|||
|
||||
const commitId = output.commitId
|
||||
|
||||
await knex.raw(
|
||||
await taskDb.raw(
|
||||
`
|
||||
UPDATE file_uploads
|
||||
SET
|
||||
|
@ -199,7 +209,7 @@ async function doTask(task) {
|
|||
)
|
||||
} catch (err) {
|
||||
taskLogger.error(err)
|
||||
await knex.raw(
|
||||
await taskDb.raw(
|
||||
`
|
||||
UPDATE file_uploads
|
||||
SET
|
||||
|
@ -208,12 +218,13 @@ async function doTask(task) {
|
|||
"convertedMessage" = ?
|
||||
WHERE "id" = ?
|
||||
`,
|
||||
[err.toString(), task.id]
|
||||
// DB only accepts a varchar 255
|
||||
[err.toString().substring(0, 254), task.id]
|
||||
)
|
||||
metricOperationErrors.labels(fileTypeForMetric).inc()
|
||||
} finally {
|
||||
const { streamId, branchName } = branchMetadata
|
||||
await knex.raw(
|
||||
await mainDb.raw(
|
||||
`NOTIFY file_import_update, '${task.id}:::${streamId}:::${branchName}:::${
|
||||
newBranchCreated ? 1 : 0
|
||||
}'`
|
||||
|
@ -226,7 +237,7 @@ async function doTask(task) {
|
|||
if (fs.existsSync(TMP_RESULTS_PATH)) fs.unlinkSync(TMP_RESULTS_PATH)
|
||||
|
||||
if (tempUserToken) {
|
||||
await serverApi.revokeTokenById(tempUserToken)
|
||||
await mainServerApi.revokeTokenById(tempUserToken)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -305,42 +316,53 @@ function wrapLogLine(line, isErr, logger) {
|
|||
logger.info({ parserLogLine: line }, 'ParserLog: {parserLogLine}')
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
if (shouldExit) {
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
try {
|
||||
const task = await startTask()
|
||||
|
||||
fs.writeFile(HEALTHCHECK_FILE_PATH, '' + Date.now(), () => {})
|
||||
|
||||
if (!task) {
|
||||
setTimeout(tick, 1000)
|
||||
return
|
||||
const doStuff = async () => {
|
||||
const dbClients = await getDbClients()
|
||||
const mainDb = dbClients.main.public
|
||||
const dbClientsIterator = infiniteDbClientsIterator(dbClients)
|
||||
while (!shouldExit) {
|
||||
const [regionName, taskDb] = dbClientsIterator.next().value
|
||||
try {
|
||||
const task = await startTask(taskDb)
|
||||
fs.writeFile(HEALTHCHECK_FILE_PATH, '' + Date.now(), () => {})
|
||||
if (!task) {
|
||||
await wait(1000)
|
||||
continue
|
||||
}
|
||||
await doTask(mainDb, regionName, taskDb, task)
|
||||
await wait(10)
|
||||
} catch (err) {
|
||||
metricOperationErrors.labels('main_loop').inc()
|
||||
logger.error(err, 'Error executing task')
|
||||
await wait(5000)
|
||||
}
|
||||
|
||||
await doTask(task)
|
||||
|
||||
// Check for another task very soon
|
||||
setTimeout(tick, 10)
|
||||
} catch (err) {
|
||||
metricOperationErrors.labels('main_loop').inc()
|
||||
logger.error(err, 'Error executing task')
|
||||
setTimeout(tick, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
logger.info('Starting FileUploads Service...')
|
||||
initPrometheusMetrics()
|
||||
await initPrometheusMetrics()
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
shouldExit = true
|
||||
logger.info('Shutting down...')
|
||||
})
|
||||
|
||||
tick()
|
||||
await doStuff()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
function* infiniteDbClientsIterator(dbClients) {
|
||||
let index = 0
|
||||
const dbClientEntries = [...Object.entries(dbClients)]
|
||||
const clientCount = dbClientEntries.length
|
||||
while (true) {
|
||||
// reset index
|
||||
if (index === clientCount) index = 0
|
||||
const [regionName, dbConnection] = dbClientEntries[index]
|
||||
index++
|
||||
yield [regionName, dbConnection.public]
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
const http = require('http')
|
||||
const prometheusClient = require('prom-client')
|
||||
const knex = require('../knex')
|
||||
const getDbClients = require('../knex')
|
||||
|
||||
let metricFree = null
|
||||
let metricUsed = null
|
||||
|
@ -24,101 +24,105 @@ prometheusClient.collectDefaultMetrics()
|
|||
|
||||
let prometheusInitialized = false
|
||||
|
||||
function initKnexPrometheusMetrics() {
|
||||
metricFree = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_free',
|
||||
help: 'Number of free DB connections',
|
||||
collect() {
|
||||
this.set(knex.client.pool.numFree())
|
||||
}
|
||||
})
|
||||
const initDBPrometheusMetricsFactory =
|
||||
({ db }) =>
|
||||
() => {
|
||||
metricFree = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_free',
|
||||
help: 'Number of free DB connections',
|
||||
collect() {
|
||||
this.set(db.client.pool.numFree())
|
||||
}
|
||||
})
|
||||
|
||||
metricUsed = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_used',
|
||||
help: 'Number of used DB connections',
|
||||
collect() {
|
||||
this.set(knex.client.pool.numUsed())
|
||||
}
|
||||
})
|
||||
metricUsed = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_used',
|
||||
help: 'Number of used DB connections',
|
||||
collect() {
|
||||
this.set(db.client.pool.numUsed())
|
||||
}
|
||||
})
|
||||
|
||||
metricPendingAquires = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending',
|
||||
help: 'Number of pending DB connection aquires',
|
||||
collect() {
|
||||
this.set(knex.client.pool.numPendingAcquires())
|
||||
}
|
||||
})
|
||||
metricPendingAquires = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending',
|
||||
help: 'Number of pending DB connection aquires',
|
||||
collect() {
|
||||
this.set(db.client.pool.numPendingAcquires())
|
||||
}
|
||||
})
|
||||
|
||||
metricPendingCreates = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending_creates',
|
||||
help: 'Number of pending DB connection creates',
|
||||
collect() {
|
||||
this.set(knex.client.pool.numPendingCreates())
|
||||
}
|
||||
})
|
||||
metricPendingCreates = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending_creates',
|
||||
help: 'Number of pending DB connection creates',
|
||||
collect() {
|
||||
this.set(db.client.pool.numPendingCreates())
|
||||
}
|
||||
})
|
||||
|
||||
metricPendingValidations = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending_validations',
|
||||
help: 'Number of pending DB connection validations. This is a state between pending acquisition and acquiring a connection.',
|
||||
collect() {
|
||||
this.set(knex.client.pool.numPendingValidations())
|
||||
}
|
||||
})
|
||||
metricPendingValidations = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending_validations',
|
||||
help: 'Number of pending DB connection validations. This is a state between pending acquisition and acquiring a connection.',
|
||||
collect() {
|
||||
this.set(db.client.pool.numPendingValidations())
|
||||
}
|
||||
})
|
||||
|
||||
metricRemainingCapacity = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_remaining_capacity',
|
||||
help: 'Remaining capacity of the DB connection pool',
|
||||
collect() {
|
||||
const postgresMaxConnections =
|
||||
parseInt(process.env.POSTGRES_MAX_CONNECTIONS_FILE_IMPORT_SERVICE) || 1
|
||||
const demand =
|
||||
knex.client.pool.numUsed() +
|
||||
knex.client.pool.numPendingCreates() +
|
||||
knex.client.pool.numPendingValidations() +
|
||||
knex.client.pool.numPendingAcquires()
|
||||
metricRemainingCapacity = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_remaining_capacity',
|
||||
help: 'Remaining capacity of the DB connection pool',
|
||||
collect() {
|
||||
const postgresMaxConnections =
|
||||
parseInt(process.env.POSTGRES_MAX_CONNECTIONS_FILE_IMPORT_SERVICE) || 1
|
||||
const demand =
|
||||
db.client.pool.numUsed() +
|
||||
db.client.pool.numPendingCreates() +
|
||||
db.client.pool.numPendingValidations() +
|
||||
db.client.pool.numPendingAcquires()
|
||||
|
||||
this.set(Math.max(postgresMaxConnections - demand, 0))
|
||||
}
|
||||
})
|
||||
this.set(Math.max(postgresMaxConnections - demand, 0))
|
||||
}
|
||||
})
|
||||
|
||||
metricQueryDuration = new prometheusClient.Summary({
|
||||
name: 'speckle_server_knex_query_duration',
|
||||
help: 'Summary of the DB query durations in seconds'
|
||||
})
|
||||
metricQueryDuration = new prometheusClient.Summary({
|
||||
name: 'speckle_server_knex_query_duration',
|
||||
help: 'Summary of the DB query durations in seconds'
|
||||
})
|
||||
|
||||
metricQueryErrors = new prometheusClient.Counter({
|
||||
name: 'speckle_server_knex_query_errors',
|
||||
help: 'Number of DB queries with errors'
|
||||
})
|
||||
metricQueryErrors = new prometheusClient.Counter({
|
||||
name: 'speckle_server_knex_query_errors',
|
||||
help: 'Number of DB queries with errors'
|
||||
})
|
||||
|
||||
knex.on('query', (data) => {
|
||||
const queryId = data.__knexQueryUid + ''
|
||||
queryStartTime[queryId] = Date.now()
|
||||
})
|
||||
db.on('query', (data) => {
|
||||
const queryId = data.__knexQueryUid + ''
|
||||
queryStartTime[queryId] = Date.now()
|
||||
})
|
||||
|
||||
knex.on('query-response', (data, obj, builder) => {
|
||||
const queryId = obj.__knexQueryUid + ''
|
||||
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
|
||||
delete queryStartTime[queryId]
|
||||
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
|
||||
})
|
||||
db.on('query-response', (data, obj, builder) => {
|
||||
const queryId = obj.__knexQueryUid + ''
|
||||
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
|
||||
delete queryStartTime[queryId]
|
||||
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
|
||||
})
|
||||
|
||||
knex.on('query-error', (err, querySpec) => {
|
||||
const queryId = querySpec.__knexQueryUid + ''
|
||||
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
|
||||
delete queryStartTime[queryId]
|
||||
db.on('query-error', (err, querySpec) => {
|
||||
const queryId = querySpec.__knexQueryUid + ''
|
||||
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
|
||||
delete queryStartTime[queryId]
|
||||
|
||||
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
|
||||
metricQueryErrors.inc()
|
||||
})
|
||||
}
|
||||
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
|
||||
metricQueryErrors.inc()
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initPrometheusMetrics() {
|
||||
async initPrometheusMetrics() {
|
||||
if (prometheusInitialized) return
|
||||
prometheusInitialized = true
|
||||
|
||||
initKnexPrometheusMetrics()
|
||||
const db = (await getDbClients()).main.public
|
||||
|
||||
initDBPrometheusMetricsFactory({ db })()
|
||||
|
||||
// Define the HTTP server
|
||||
const server = http.createServer(async (req, res) => {
|
||||
|
|
|
@ -69,6 +69,8 @@ if __name__ == "__main__":
|
|||
|
||||
try:
|
||||
commit_id = import_stl()
|
||||
if isinstance(commit_id, Exception):
|
||||
raise commit_id
|
||||
results = {"success": True, "commitId": commit_id}
|
||||
except Exception as ex:
|
||||
results = {"success": False, "error": str(ex)}
|
||||
|
|
|
@ -42,3 +42,5 @@ NUXT_PUBLIC_ENABLE_DIRECT_PREVIEWS=true
|
|||
|
||||
# Ghost API
|
||||
NUXT_PUBLIC_GHOST_API_KEY=
|
||||
|
||||
NUXT_WEBFLOW_API_TOKEN=8c9bea4c120742a21ff308cda0bea73f13e89ffe26dcc886990bba353549b652
|
|
@ -4,7 +4,7 @@
|
|||
<slot name="icon" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div v-if="title || description" class="flex-1">
|
||||
<div v-if="title" class="flex items-center gap-2">
|
||||
<p class="text-heading-sm text-foreground">{{ title }}</p>
|
||||
<CommonBadge v-if="badge" rounded>{{ badge }}</CommonBadge>
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<NuxtLink :to="webflowItem.url" target="_blank">
|
||||
<div
|
||||
class="bg-foundation border border-outline-3 rounded-xl flex flex-col overflow-hidden hover:border-outline-5 transition"
|
||||
>
|
||||
<NuxtImg
|
||||
v-if="webflowItem.featureImageUrl"
|
||||
:src="webflowItem.featureImageUrl"
|
||||
:alt="webflowItem.title"
|
||||
class="h-32 w-full object-cover"
|
||||
width="400"
|
||||
height="225"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="bg-foundation-page w-full h-32 flex items-center justify-center"
|
||||
>
|
||||
<HeaderLogoBlock no-link minimal class="scale-150" />
|
||||
</div>
|
||||
<div class="p-5 pb-4">
|
||||
<h3 class="text-body-2xs text-foreground truncate">
|
||||
{{ webflowItem.title }}
|
||||
</h3>
|
||||
<p class="text-body-3xs text-foreground-2 mt-2">
|
||||
<span v-tippy="createdOn.full">
|
||||
{{ createdOn.relative }}
|
||||
</span>
|
||||
<template v-if="webflowItem.readTime">
|
||||
<span class="pl-1 pr-2">•</span>
|
||||
{{ webflowItem.readTime }}m read
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { WebflowItem } from '~/lib/dashboard/helpers/types'
|
||||
|
||||
const props = defineProps<{
|
||||
webflowItem: WebflowItem
|
||||
}>()
|
||||
|
||||
const createdOn = computed(() => ({
|
||||
full: formattedFullDate(props.webflowItem.createdOn),
|
||||
relative: formattedRelativeDate(props.webflowItem.createdOn, { capitalize: true })
|
||||
}))
|
||||
</script>
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<section v-if="!error">
|
||||
<h2 class="text-heading-sm text-foreground-2 mb-4">Blog</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<DashboardBlogCard
|
||||
v-for="webflowItem in webflowItems"
|
||||
:key="webflowItem.id"
|
||||
:webflow-item="webflowItem"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section v-else />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { WebflowItem } from '~/lib/dashboard/helpers/types'
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
const { data: webflowData, error } = await useAsyncData<{
|
||||
items: WebflowItem[]
|
||||
}>('webflow-items', () =>
|
||||
$fetch('/api/webflow', {
|
||||
onResponseError({ response }) {
|
||||
logger.error('API Response Error:', response.status, response.statusText)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const webflowItems = computed(() => webflowData.value?.items || [])
|
||||
</script>
|
|
@ -152,14 +152,6 @@
|
|||
</NuxtLink>
|
||||
</LayoutSidebarMenuGroup>
|
||||
</LayoutSidebarMenu>
|
||||
<template #promo>
|
||||
<LayoutSidebarPromo
|
||||
title="SpeckleCon 2024"
|
||||
text="Join us in London on Nov 13-14 for the ultimate community event."
|
||||
button-text="Get tickets"
|
||||
@on-click="onPromoClick"
|
||||
/>
|
||||
</template>
|
||||
</LayoutSidebar>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -177,7 +169,6 @@
|
|||
import {
|
||||
FormButton,
|
||||
LayoutSidebar,
|
||||
LayoutSidebarPromo,
|
||||
LayoutSidebarMenu,
|
||||
LayoutSidebarMenuGroup,
|
||||
LayoutSidebarMenuGroupItem
|
||||
|
@ -248,15 +239,6 @@ onWorkspaceResult((result) => {
|
|||
}
|
||||
})
|
||||
|
||||
const onPromoClick = () => {
|
||||
mixpanel.track('Promo Banner Clicked', {
|
||||
source: 'sidebar',
|
||||
campaign: 'specklecon2024'
|
||||
})
|
||||
|
||||
window.open('https://conf.speckle.systems/', '_blank')
|
||||
}
|
||||
|
||||
const openFeedbackDialog = () => {
|
||||
showFeedbackDialog.value = true
|
||||
isOpenMobile.value = false
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
<template>
|
||||
<NuxtLink :to="tutorial.url" target="_blank">
|
||||
<div
|
||||
class="bg-foundation border border-outline-3 rounded-xl flex flex-col overflow-hidden hover:border-outline-5 transition"
|
||||
>
|
||||
<div
|
||||
:style="{ backgroundImage: `url(${tutorial.featureImage})` }"
|
||||
class="bg-foundation-page bg-cover bg-center w-full h-32"
|
||||
/>
|
||||
<div class="p-5 pb-4">
|
||||
<h3 v-if="tutorial.title" class="text-body-2xs text-foreground truncate">
|
||||
{{ tutorial.title }}
|
||||
</h3>
|
||||
<p class="text-body-3xs text-foreground-2 mt-2">
|
||||
<span v-tippy="updatedAt.full">
|
||||
{{ updatedAt.relative }}
|
||||
</span>
|
||||
<template v-if="tutorial.readingTime">
|
||||
<span class="pl-1 pr-2">•</span>
|
||||
{{ tutorial.readingTime }}m read
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TutorialItem } from '~~/lib/dashboard/helpers/types'
|
||||
|
||||
const props = defineProps<{
|
||||
tutorial: TutorialItem
|
||||
}>()
|
||||
|
||||
const updatedAt = computed(() => {
|
||||
return {
|
||||
full: formattedFullDate(props.tutorial.publishedAt),
|
||||
relative: formattedRelativeDate(props.tutorial.publishedAt, { capitalize: true })
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -6,7 +6,7 @@
|
|||
:on-submit="onSubmit"
|
||||
max-width="md"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-body-xs text-foreground font-medium">
|
||||
How can we improve Speckle? If you have a feature request, please also share how
|
||||
you would use it and why it's important to you
|
||||
|
@ -18,6 +18,13 @@
|
|||
label="Feedback"
|
||||
color="foundation"
|
||||
/>
|
||||
<p class="text-body-xs !leading-4">
|
||||
Need help? For support, head over to our
|
||||
<FormButton to="https://speckle.community/" target="_blank" link text>
|
||||
community forum
|
||||
</FormButton>
|
||||
where we can chat and solve problems together.
|
||||
</p>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
:fully-control-value="fullyControlValue"
|
||||
:disabled="disabled"
|
||||
:disabled-item-predicate="disabledItemPredicate"
|
||||
:disabled-item-tooltip="disabledItemTooltip"
|
||||
:clearable="clearable"
|
||||
>
|
||||
<template #nothing-selected>
|
||||
|
@ -82,6 +83,10 @@ const props = defineProps({
|
|||
required: false,
|
||||
type: Array as PropType<WorkspaceRoles[]>
|
||||
},
|
||||
currentRole: {
|
||||
type: String as PropType<WorkspaceRoles>,
|
||||
required: false
|
||||
},
|
||||
showLabel: Boolean,
|
||||
clearable: Boolean,
|
||||
hideItems: {
|
||||
|
@ -115,8 +120,15 @@ const roles = computed(() => {
|
|||
return Object.values(Roles.Workspace)
|
||||
})
|
||||
|
||||
const disabledItemTooltip = computed(() =>
|
||||
props.currentRole
|
||||
? `User is already a ${RoleInfo.Workspace[props.currentRole].title}`
|
||||
: undefined
|
||||
)
|
||||
|
||||
const disabledItemPredicate = (item: WorkspaceRoles) =>
|
||||
props.disabledItems && props.disabledItems.length > 0
|
||||
? props.disabledItems.includes(item)
|
||||
: false
|
||||
(props.disabledItems &&
|
||||
props.disabledItems.length > 0 &&
|
||||
props.disabledItems.includes(item)) ||
|
||||
item === props.currentRole
|
||||
</script>
|
||||
|
|
|
@ -9,16 +9,16 @@
|
|||
viewBox="0 0 8 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-outline-2"
|
||||
class="text-outline-5"
|
||||
>
|
||||
<path d="M2 18L6 6" stroke="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
<NuxtLink
|
||||
:to="disableLink ? undefined : to"
|
||||
class="flex gap-1 items-center text-body-xs ml-0.5 text-foreground-2 select-none truncate font-medium"
|
||||
class="flex gap-1 items-center text-body-xs ml-0.5 text-foreground-2 select-none truncate"
|
||||
:class="disableLink ? '' : 'hover:!text-foreground'"
|
||||
active-class="group is-active !text-foreground"
|
||||
active-class="group is-active !text-foreground font-medium"
|
||||
>
|
||||
<div class="truncate">
|
||||
{{ name || to }}
|
||||
|
|
|
@ -98,7 +98,7 @@ const token = computed(
|
|||
)
|
||||
const mainClasses = computed(() => {
|
||||
const classParts = [
|
||||
'flex flex-col space-y-4 px-4 py-5 transition border-x border-b border-outline-2 first:border-t first:rounded-t-lg last:rounded-b-lg'
|
||||
'flex flex-col space-y-4 px-4 py-5 transition bg-foundation border-x border-b border-outline-2 first:border-t first:rounded-t-lg last:rounded-b-lg'
|
||||
]
|
||||
|
||||
if (props.block) {
|
||||
|
|
|
@ -53,7 +53,7 @@ const emit = defineEmits<{
|
|||
const props = defineProps<{
|
||||
versions: ProjectModelPageDialogDeleteVersionFragment[]
|
||||
open: boolean
|
||||
projectId?: string
|
||||
projectId: string
|
||||
modelId?: string
|
||||
}>()
|
||||
|
||||
|
@ -71,10 +71,10 @@ const onDelete = async () => {
|
|||
loading.value = true
|
||||
const success = await deleteVersions(
|
||||
{
|
||||
projectId: props.projectId,
|
||||
versionIds: props.versions.map((v) => v.id)
|
||||
},
|
||||
{
|
||||
projectId: props.projectId,
|
||||
modelId: props.modelId
|
||||
}
|
||||
)
|
||||
|
|
|
@ -57,6 +57,7 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: string
|
||||
version: Nullable<ProjectModelPageDialogDeleteVersionFragment>
|
||||
open: boolean
|
||||
}>()
|
||||
|
@ -85,6 +86,7 @@ const onSubmit = handleSubmit(async ({ newMessage }) => {
|
|||
|
||||
loading.value = true
|
||||
const success = !!(await updateVersion({
|
||||
projectId: props.projectId,
|
||||
versionId: props.version?.id,
|
||||
message: newMessage
|
||||
}))
|
||||
|
|
|
@ -81,13 +81,13 @@ const onMove = async (targetModelName: string, newModelCreated?: boolean) => {
|
|||
loading.value = true
|
||||
const success = await moveVersions(
|
||||
{
|
||||
projectId: props.projectId,
|
||||
versionIds: props.versions.map((v) => v.id),
|
||||
targetModelName
|
||||
},
|
||||
{
|
||||
previousModelId: props.modelId,
|
||||
newModelCreated,
|
||||
projectId: props.projectId
|
||||
newModelCreated
|
||||
}
|
||||
)
|
||||
loading.value = false
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
<template>
|
||||
<ProjectEmptyState
|
||||
:small="small"
|
||||
title="No discussions, yet."
|
||||
:text="small ? undefined : 'Open a model and start the collaboration today!'"
|
||||
>
|
||||
<ProjectEmptyState :small="small" title="No discussions, yet." :text="text">
|
||||
<template #cta>
|
||||
<div v-if="showButton" class="mt-3">
|
||||
<FormButton :icon-left="PlusIcon" @click="() => $emit('new-discussion')">
|
||||
|
@ -23,5 +19,6 @@ defineEmits<{
|
|||
defineProps<{
|
||||
small?: boolean
|
||||
showButton?: boolean
|
||||
text?: string
|
||||
}>()
|
||||
</script>
|
||||
|
|
|
@ -118,7 +118,8 @@ const emit = defineEmits<{
|
|||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const createProject = useCreateProject()
|
||||
const router = useRouter()
|
||||
const { handleSubmit, meta } = useForm<FormValues>()
|
||||
const logger = useLogger()
|
||||
const { handleSubmit, meta, isSubmitting } = useForm<FormValues>()
|
||||
const { result: workspaceResult } = useQuery(projectWorkspaceSelectQuery, null, () => ({
|
||||
enabled: isWorkspacesEnabled.value
|
||||
}))
|
||||
|
@ -127,43 +128,62 @@ const visibility = ref(ProjectVisibility.Unlisted)
|
|||
const selectedWorkspace = ref<ProjectsAddDialog_WorkspaceFragment>()
|
||||
const showConfirmDialog = ref(false)
|
||||
const confirmActionType = ref<'navigate' | 'close' | null>(null)
|
||||
const isClosing = ref(false)
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const mp = useMixpanel()
|
||||
|
||||
const onSubmit = handleSubmit(async (values) => {
|
||||
await createProject({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
visibility: visibility.value,
|
||||
workspaceId: props.workspaceId || selectedWorkspace.value?.id
|
||||
})
|
||||
emit('created')
|
||||
mp.track('Stream Action', {
|
||||
type: 'action',
|
||||
name: 'create',
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: props.workspaceId
|
||||
})
|
||||
open.value = false
|
||||
if (isClosing.value) return // Prevent submission while closing
|
||||
|
||||
try {
|
||||
isClosing.value = true
|
||||
const workspaceId = props.workspaceId || selectedWorkspace.value?.id
|
||||
|
||||
await createProject({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
visibility: visibility.value,
|
||||
...(workspaceId ? { workspaceId } : {})
|
||||
})
|
||||
emit('created')
|
||||
mp.track('Stream Action', {
|
||||
type: 'action',
|
||||
name: 'create',
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: props.workspaceId
|
||||
})
|
||||
open.value = false
|
||||
} catch (error) {
|
||||
isClosing.value = false
|
||||
logger.error('Failed to create project:', error)
|
||||
}
|
||||
})
|
||||
|
||||
const workspaces = computed(
|
||||
() => workspaceResult.value?.activeUser?.workspaces.items ?? []
|
||||
)
|
||||
const hasWorkspaces = computed(() => workspaces.value.length > 0)
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => {
|
||||
const isDisabled = isSubmitting.value || isClosing.value
|
||||
|
||||
return [
|
||||
{
|
||||
text: 'Cancel',
|
||||
props: { color: 'outline' },
|
||||
props: {
|
||||
color: 'outline',
|
||||
disabled: isDisabled
|
||||
},
|
||||
onClick: confirmCancel
|
||||
},
|
||||
{
|
||||
text: 'Create',
|
||||
props: {
|
||||
submit: true
|
||||
submit: true,
|
||||
loading: isDisabled,
|
||||
disabled: isDisabled
|
||||
},
|
||||
onClick: onSubmit
|
||||
}
|
||||
|
@ -204,6 +224,7 @@ const handleConfirmAction = () => {
|
|||
watch(open, (newVal, oldVal) => {
|
||||
if (newVal && !oldVal) {
|
||||
selectedWorkspace.value = undefined
|
||||
isClosing.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="hasBanners"
|
||||
class="bg-foundation divide-y divide-outline-3 mb-8 empty:mb-0"
|
||||
>
|
||||
<div v-if="hasBanners" class="mb-8 empty:mb-0">
|
||||
<ProjectsInviteBanners
|
||||
v-if="projectsInvites?.projectInvites?.length"
|
||||
:invites="projectsInvites"
|
||||
/>
|
||||
<WorkspaceInviteBanners
|
||||
v-if="
|
||||
workspacesInvites?.workspaceInvites?.length ||
|
||||
workspacesInvites?.discoverableWorkspaces?.length
|
||||
"
|
||||
:invites="workspacesInvites"
|
||||
<WorkspaceInviteBanner
|
||||
v-for="invite in workspaceInvites"
|
||||
:key="invite.id"
|
||||
:invite="invite"
|
||||
/>
|
||||
<WorkspaceInviteDiscoverableWorkspaceBanner
|
||||
v-for="workspace in discoverableWorkspaces"
|
||||
:key="workspace.id"
|
||||
:workspace="workspace"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useSynchronizedCookie } from '~/lib/common/composables/reactiveCookie'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type {
|
||||
ProjectsDashboardHeaderProjects_UserFragment,
|
||||
ProjectsDashboardHeaderWorkspaces_UserFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { CookieKeys } from '~/lib/common/helpers/constants'
|
||||
|
||||
graphql(`
|
||||
fragment ProjectsDashboardHeaderProjects_User on User {
|
||||
|
@ -33,7 +35,12 @@ graphql(`
|
|||
|
||||
graphql(`
|
||||
fragment ProjectsDashboardHeaderWorkspaces_User on User {
|
||||
...WorkspaceInviteBanners_User
|
||||
discoverableWorkspaces {
|
||||
...WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace
|
||||
}
|
||||
workspaceInvites {
|
||||
...WorkspaceInviteBanner_PendingWorkspaceCollaborator
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
|
@ -42,11 +49,26 @@ const props = defineProps<{
|
|||
workspacesInvites?: ProjectsDashboardHeaderWorkspaces_UserFragment
|
||||
}>()
|
||||
|
||||
const dismissedDiscoverableWorkspaces = useSynchronizedCookie<string[]>(
|
||||
CookieKeys.DismissedDiscoverableWorkspaces,
|
||||
{
|
||||
default: () => []
|
||||
}
|
||||
)
|
||||
|
||||
const workspaceInvites = computed(() => props.workspacesInvites?.workspaceInvites || [])
|
||||
const discoverableWorkspaces = computed(
|
||||
() =>
|
||||
props.workspacesInvites?.discoverableWorkspaces?.filter(
|
||||
(workspace) => !dismissedDiscoverableWorkspaces.value.includes(workspace.id)
|
||||
) || []
|
||||
)
|
||||
|
||||
const hasBanners = computed(() => {
|
||||
return (
|
||||
props.projectsInvites?.projectInvites?.length ||
|
||||
props.workspacesInvites?.workspaceInvites?.length ||
|
||||
props.workspacesInvites?.discoverableWorkspaces?.length
|
||||
workspaceInvites.value.length ||
|
||||
discoverableWorkspaces.value.length
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -20,11 +20,7 @@
|
|||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="text"
|
||||
class="text-body-xs text-foreground-2 pt-1"
|
||||
:class="{ 'pt-6': subheading }"
|
||||
>
|
||||
<p v-if="text" class="text-body-xs text-foreground-2 pt-1">
|
||||
{{ text }}
|
||||
</p>
|
||||
<hr v-if="!subheading && !hideDivider" class="my-6 border-outline-2" />
|
||||
|
|
|
@ -161,11 +161,11 @@ const showActionsMenu = ref<Record<string, boolean>>({})
|
|||
const actionItems: LayoutMenuItem[][] = [
|
||||
[
|
||||
{
|
||||
title: 'Change role',
|
||||
title: 'Change role...',
|
||||
id: ActionTypes.ChangeRole
|
||||
},
|
||||
{
|
||||
title: 'Remove user',
|
||||
title: 'Remove user...',
|
||||
id: ActionTypes.RemoveUser
|
||||
}
|
||||
]
|
||||
|
|
|
@ -129,7 +129,7 @@ const invites = computed(() => result.value?.admin.inviteList.items || [])
|
|||
const actionItems: LayoutMenuItem[][] = [
|
||||
[
|
||||
{ title: 'Resend invitation', id: 'resend-invite' },
|
||||
{ title: 'Delete invitation', id: 'delete-invite' }
|
||||
{ title: 'Delete invitation...', id: 'delete-invite' }
|
||||
]
|
||||
]
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<section>
|
||||
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0">
|
||||
<SettingsSectionHeader
|
||||
title="Regions"
|
||||
text="Manage the regions available for customizing data residency"
|
||||
/>
|
||||
<div class="flex flex-col space-y-6">
|
||||
<div class="flex flex-row-reverse">
|
||||
<div v-tippy="disabledMessage">
|
||||
<FormButton :disabled="!canCreateRegion" @click="onCreate">
|
||||
Create
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsServerRegionsTable :items="tableItems" @edit="onEditRegion" />
|
||||
</div>
|
||||
</div>
|
||||
<SettingsServerRegionsAddEditDialog
|
||||
v-model="editModel"
|
||||
v-model:open="isAddEditDialogOpen"
|
||||
:available-region-keys="availableKeys"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import type { SettingsServerRegionsTable_ServerRegionItemFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
const isAddEditDialogOpen = ref(false)
|
||||
|
||||
const query = graphql(`
|
||||
query SettingsServerRegions {
|
||||
serverInfo {
|
||||
multiRegion {
|
||||
regions {
|
||||
id
|
||||
...SettingsServerRegionsTable_ServerRegionItem
|
||||
}
|
||||
availableKeys
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const editModel = ref<SettingsServerRegionsTable_ServerRegionItemFragment>()
|
||||
|
||||
const pageFetchPolicy = usePageQueryStandardFetchPolicy()
|
||||
const { result } = useQuery(query, undefined, () => ({
|
||||
fetchPolicy: pageFetchPolicy.value
|
||||
}))
|
||||
|
||||
const tableItems = computed(() => result.value?.serverInfo?.multiRegion?.regions)
|
||||
const availableKeys = computed(
|
||||
() => result.value?.serverInfo?.multiRegion?.availableKeys || []
|
||||
)
|
||||
const canCreateRegion = computed(() => availableKeys.value.length > 0)
|
||||
const disabledMessage = computed(() => {
|
||||
if (canCreateRegion.value) return undefined
|
||||
|
||||
if (!availableKeys.value.length) return 'No available region keys'
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
const onCreate = () => {
|
||||
editModel.value = undefined
|
||||
isAddEditDialogOpen.value = true
|
||||
}
|
||||
|
||||
const onEditRegion = (item: SettingsServerRegionsTable_ServerRegionItemFragment) => {
|
||||
editModel.value = item
|
||||
isAddEditDialogOpen.value = true
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="open"
|
||||
max-width="sm"
|
||||
:buttons="dialogButtons"
|
||||
hide-closer
|
||||
prevent-close-on-click-outside
|
||||
:on-submit="onSubmit"
|
||||
>
|
||||
<template #header>Create a new region</template>
|
||||
<div class="flex flex-col gap-y-4 mb-2">
|
||||
<FormTextInput
|
||||
name="name"
|
||||
label="Region name"
|
||||
placeholder="Name"
|
||||
color="foundation"
|
||||
:rules="[isRequired, isStringOfLength({ maxLength: 64 })]"
|
||||
auto-focus
|
||||
autocomplete="off"
|
||||
show-required
|
||||
show-label
|
||||
help="Human readable name for the region."
|
||||
/>
|
||||
<FormTextArea
|
||||
name="description"
|
||||
label="Region description"
|
||||
placeholder="Description"
|
||||
color="foundation"
|
||||
size="lg"
|
||||
show-label
|
||||
show-optional
|
||||
:rules="[isStringOfLength({ maxLength: 65536 })]"
|
||||
/>
|
||||
<SettingsServerRegionsKeySelect
|
||||
show-label
|
||||
name="key"
|
||||
:items="availableRegionKeys"
|
||||
label="Region key"
|
||||
:rules="[isRequired]"
|
||||
show-required
|
||||
help="These keys come from the server multi region configuration file."
|
||||
/>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { isRequired, isStringOfLength } from '~~/lib/common/helpers/validation'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { SettingsServerRegionsAddEditDialog_ServerRegionItemFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { useForm } from 'vee-validate'
|
||||
import {
|
||||
useCreateRegion,
|
||||
useUpdateRegion
|
||||
} from '~/lib/multiregion/composables/management'
|
||||
import { useMutationLoading } from '@vue/apollo-composable'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {
|
||||
id
|
||||
name
|
||||
description
|
||||
key
|
||||
}
|
||||
`)
|
||||
|
||||
type ServerRegionItem = SettingsServerRegionsAddEditDialog_ServerRegionItemFragment
|
||||
type DialogModel = Omit<ServerRegionItem, 'id'>
|
||||
|
||||
defineProps<{
|
||||
availableRegionKeys: string[]
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
const model = defineModel<DialogModel>()
|
||||
const { handleSubmit, setValues } = useForm<DialogModel>()
|
||||
const createRegion = useCreateRegion()
|
||||
const updateRegion = useUpdateRegion()
|
||||
const loading = useMutationLoading()
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => {
|
||||
return [
|
||||
{
|
||||
text: 'Cancel',
|
||||
props: { color: 'outline' },
|
||||
onClick: () => (open.value = false)
|
||||
},
|
||||
{
|
||||
text: isEditMode.value ? 'Update' : 'Create',
|
||||
props: {
|
||||
submit: true,
|
||||
disabled: loading.value
|
||||
},
|
||||
onClick: noop
|
||||
}
|
||||
]
|
||||
})
|
||||
const isEditMode = computed(() => !!model.value)
|
||||
|
||||
const onSubmit = handleSubmit(async (values) => {
|
||||
const action = isEditMode.value ? updateRegion : createRegion
|
||||
const res = await action({
|
||||
input: {
|
||||
key: values.key,
|
||||
name: values.name,
|
||||
description: values.description
|
||||
}
|
||||
})
|
||||
|
||||
if (res?.id) {
|
||||
open.value = false
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
model,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal && newVal !== oldVal) {
|
||||
setValues(newVal)
|
||||
} else if (!newVal && oldVal) {
|
||||
setValues({ name: '', description: '', key: '' })
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<FormSelectBase
|
||||
v-bind="props"
|
||||
v-model="selectedValue"
|
||||
:name="name || 'regions-key'"
|
||||
:allow-unset="false"
|
||||
mount-menu-on-body
|
||||
>
|
||||
<template #option="{ item }">
|
||||
<div class="flex items-center">
|
||||
<span class="truncate">{{ item }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #nothing-selected>
|
||||
{{ multiple ? 'Select region keys' : 'Select a region key' }}
|
||||
</template>
|
||||
<template #something-selected="{ value }">
|
||||
<template v-if="isArray(value)">
|
||||
{{ value.join(', ') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ value }}
|
||||
</template>
|
||||
</template>
|
||||
</FormSelectBase>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { isArray } from 'lodash-es'
|
||||
import type { RuleExpression } from 'vee-validate'
|
||||
import { useFormSelectChildInternals } from '~/lib/form/composables/select'
|
||||
|
||||
type ValueType = string | string[] | undefined
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: ValueType): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: ValueType
|
||||
label: string
|
||||
items: string[]
|
||||
multiple?: boolean
|
||||
name?: string
|
||||
showOptional?: boolean
|
||||
showRequired?: boolean
|
||||
showLabel?: boolean
|
||||
labelId?: string
|
||||
buttonId?: string
|
||||
help?: string
|
||||
rules?: RuleExpression<string | string[] | undefined>
|
||||
}>()
|
||||
|
||||
const { selectedValue } = useFormSelectChildInternals<string>({
|
||||
props: toRefs(props),
|
||||
emit
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div>
|
||||
<LayoutTable
|
||||
:items="items"
|
||||
:columns="[
|
||||
{ id: 'name', header: 'Name', classes: 'col-span-3 truncate' },
|
||||
{ id: 'key', header: 'Key', classes: 'col-span-2 truncate' },
|
||||
{ id: 'description', header: 'Description', classes: 'col-span-6 truncate' },
|
||||
{ id: 'actions', header: '', classes: 'col-span-1' }
|
||||
]"
|
||||
empty-message="No regions defined"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #key="{ item }">
|
||||
<span class="text-foreground-2">{{ item.key }}</span>
|
||||
</template>
|
||||
<template #description="{ item }">
|
||||
<span class="text-foreground-2">{{ item.description }}</span>
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<LayoutMenu
|
||||
v-model:open="showActionsMenu[item.id]"
|
||||
:items="actionItems"
|
||||
mount-menu-on-body
|
||||
:menu-position="HorizontalDirection.Left"
|
||||
@chosen="({ item: actionItem }) => onActionChosen(actionItem, item)"
|
||||
>
|
||||
<FormButton
|
||||
:color="showActionsMenu[item.id] ? 'outline' : 'subtle'"
|
||||
hide-text
|
||||
:icon-right="showActionsMenu[item.id] ? XMarkIcon : EllipsisHorizontalIcon"
|
||||
@click.stop="toggleMenu(item.id)"
|
||||
/>
|
||||
</LayoutMenu>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { LayoutMenuItem } from '@speckle/ui-components'
|
||||
import { HorizontalDirection } from '~~/lib/common/composables/window'
|
||||
import { EllipsisHorizontalIcon, XMarkIcon } from '@heroicons/vue/24/outline'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { SettingsServerRegionsTable_ServerRegionItemFragment } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {
|
||||
id
|
||||
name
|
||||
key
|
||||
description
|
||||
}
|
||||
`)
|
||||
|
||||
enum ActionTypes {
|
||||
Edit = 'edit'
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
edit: [item: SettingsServerRegionsTable_ServerRegionItemFragment]
|
||||
}>()
|
||||
|
||||
defineProps<{
|
||||
items: SettingsServerRegionsTable_ServerRegionItemFragment[] | undefined
|
||||
}>()
|
||||
|
||||
const showActionsMenu = ref<Record<string, boolean>>({})
|
||||
const actionItems: LayoutMenuItem[][] = [
|
||||
[{ title: 'Edit region', id: ActionTypes.Edit }]
|
||||
]
|
||||
|
||||
const toggleMenu = (itemId: string) => {
|
||||
showActionsMenu.value[itemId] = !showActionsMenu.value[itemId]
|
||||
}
|
||||
|
||||
const onActionChosen = (
|
||||
actionItem: LayoutMenuItem,
|
||||
item: SettingsServerRegionsTable_ServerRegionItemFragment
|
||||
) => {
|
||||
if (actionItem.id === ActionTypes.Edit) {
|
||||
emit('edit', item)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -7,6 +7,7 @@
|
|||
label="New role"
|
||||
fully-control-value
|
||||
:disabled-items="disabledItems"
|
||||
:current-role="currentRole"
|
||||
show-label
|
||||
show-description
|
||||
/>
|
||||
|
@ -43,8 +44,8 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
workspaceDomainPolicyCompliant?: boolean | null
|
||||
currentRole?: WorkspaceRoles
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
|
|
|
@ -177,8 +177,8 @@ const showActionsMenu = ref<Record<string, boolean>>({})
|
|||
|
||||
const actionItems: LayoutMenuItem[][] = [
|
||||
[
|
||||
{ title: 'View project...', id: ActionTypes.ViewProject },
|
||||
{ title: 'Edit members...', id: ActionTypes.EditMembers },
|
||||
{ title: 'View project', id: ActionTypes.ViewProject },
|
||||
{ title: 'Edit members', id: ActionTypes.EditMembers },
|
||||
{ title: 'Remove project...', id: ActionTypes.RemoveProject }
|
||||
]
|
||||
]
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
getFirstErrorMessage,
|
||||
convertThrowIntoFetchResult
|
||||
} from '~~/lib/common/helpers/graphql'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsUserEmails_User on User {
|
||||
|
@ -58,6 +59,7 @@ const { handleSubmit } = useForm<FormValues>()
|
|||
const { triggerNotification } = useGlobalToast()
|
||||
const { result: userEmailsResult } = useQuery(settingsUserEmailsQuery)
|
||||
const { mutate: createMutation } = useMutation(settingsCreateUserEmailMutation)
|
||||
const mixpanel = useMixpanel()
|
||||
|
||||
const email = ref('')
|
||||
|
||||
|
@ -81,6 +83,8 @@ const onAddEmailSubmit = handleSubmit(async () => {
|
|||
type: ToastNotificationType.Success,
|
||||
title: `${email.value} added`
|
||||
})
|
||||
|
||||
mixpanel.track('Email Added')
|
||||
} else {
|
||||
const errorMessage = getFirstErrorMessage(result?.errors)
|
||||
triggerNotification({
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
getFirstErrorMessage,
|
||||
convertThrowIntoFetchResult
|
||||
} from '~~/lib/common/helpers/graphql'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
|
||||
const props = defineProps<{
|
||||
emailId: string
|
||||
|
@ -31,6 +32,7 @@ const isOpen = defineModel<boolean>('open', { required: true })
|
|||
|
||||
const { mutate: deleteMutation } = useMutation(settingsDeleteUserEmailMutation)
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const mixpanel = useMixpanel()
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
|
@ -58,6 +60,8 @@ const onDeleteEmail = async () => {
|
|||
type: ToastNotificationType.Success,
|
||||
title: `${props.email} deleted`
|
||||
})
|
||||
|
||||
mixpanel.track('Email Deleted')
|
||||
} else {
|
||||
const errorMessage = getFirstErrorMessage(result?.errors)
|
||||
triggerNotification({
|
||||
|
|
|
@ -22,6 +22,9 @@ import {
|
|||
getFirstErrorMessage,
|
||||
convertThrowIntoFetchResult
|
||||
} from '~~/lib/common/helpers/graphql'
|
||||
import { resolveMixpanelUserId } from '@speckle/shared'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
|
||||
const props = defineProps<{
|
||||
emailId: string
|
||||
|
@ -31,6 +34,8 @@ const isOpen = defineModel<boolean>('open', { required: true })
|
|||
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const { mutate: updateMutation } = useMutation(settingsSetPrimaryUserEmailMutation)
|
||||
const mixpanel = useMixpanel()
|
||||
const { distinctId } = useActiveUser()
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
|
@ -51,6 +56,8 @@ const dialogButtons = computed((): LayoutDialogButton[] => [
|
|||
])
|
||||
|
||||
const onSetPrimary = async () => {
|
||||
// Create a copy of the original email to use in the alias event before it's overwritten
|
||||
const originalDistinctId = toRaw(distinctId.value)
|
||||
const result = await updateMutation({ input: { id: props.emailId } }).catch(
|
||||
convertThrowIntoFetchResult
|
||||
)
|
||||
|
@ -59,6 +66,12 @@ const onSetPrimary = async () => {
|
|||
type: ToastNotificationType.Success,
|
||||
title: `Made ${props.email} primary`
|
||||
})
|
||||
|
||||
if (originalDistinctId) {
|
||||
mixpanel.alias(resolveMixpanelUserId(props.email), originalDistinctId)
|
||||
}
|
||||
|
||||
mixpanel.track('Primary Email Changed')
|
||||
} else {
|
||||
const errorMessage = getFirstErrorMessage(result?.errors)
|
||||
triggerNotification({
|
||||
|
|
|
@ -1,96 +1,213 @@
|
|||
<template>
|
||||
<section>
|
||||
<div class="md:max-w-xl md:mx-auto pb-6 md:pb-0">
|
||||
<div class="md:max-w-4xl md:mx-auto pb-6 md:pb-0">
|
||||
<SettingsSectionHeader title="Billing" text="Your workspace billing details" />
|
||||
<CommonCard v-if="versionCount" class="text-body-xs bg-foundation">
|
||||
<p class="text-foreground font-medium">Workspaces are free while in beta.</p>
|
||||
<p class="py-6">
|
||||
Once the beta period ends, workspaces are still free up to
|
||||
{{ versionCount.max }} model versions.
|
||||
<br />
|
||||
To store more versions across your projects you will need to upgrade to a paid
|
||||
plan.
|
||||
</p>
|
||||
<CommonProgressBar
|
||||
class="my-3"
|
||||
:current-value="versionCount.current"
|
||||
:max-value="versionCount.max"
|
||||
/>
|
||||
<div class="flex flex-row justify-between">
|
||||
<p class="text-foreground-2">
|
||||
Current model versions:
|
||||
<span class="text-foreground">{{ versionCount.current }}</span>
|
||||
</p>
|
||||
<p class="text-foreground-2">
|
||||
Free model versions limit:
|
||||
<span class="text-foreground">{{ versionCount.max }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</CommonCard>
|
||||
<template v-if="isBillingIntegrationEnabled">
|
||||
<div class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<SettingsSectionHeader title="Billing summary" subheading class="pt-4" />
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<CommonCard class="gap-y-1 bg-foundation">
|
||||
<p class="text-body-xs text-foreground-2 font-medium">
|
||||
{{ isTrialPeriod ? 'Trial plan' : 'Current plan' }}
|
||||
</p>
|
||||
<h4 class="text-heading-lg text-foreground capitalize">
|
||||
{{ currentPlan?.name }} plan
|
||||
</h4>
|
||||
<p
|
||||
v-if="currentPlan?.name && subscription?.billingInterval"
|
||||
class="text-body-xs text-foreground-2"
|
||||
>
|
||||
£{{ seatPrice }} per seat/month, billed
|
||||
{{ subscription?.billingInterval }}
|
||||
</p>
|
||||
</CommonCard>
|
||||
<CommonCard class="gap-y-1 bg-foundation">
|
||||
<p class="text-body-xs text-foreground-2">
|
||||
{{
|
||||
isTrialPeriod
|
||||
? 'Expected bill'
|
||||
: subscription?.billingInterval === BillingInterval.Monthly
|
||||
? 'Monthly bill'
|
||||
: 'Yearly bill'
|
||||
}}
|
||||
</p>
|
||||
<h4 class="text-heading-lg text-foreground capitalize">Coming soon</h4>
|
||||
</CommonCard>
|
||||
<CommonCard class="gap-y-1 bg-foundation">
|
||||
<p class="text-body-xs text-foreground-2">
|
||||
{{ isTrialPeriod ? 'First payment due' : 'Next payment due' }}
|
||||
</p>
|
||||
<h4 class="text-heading-lg text-foreground capitalize">
|
||||
{{
|
||||
isPaidPlan
|
||||
? dayjs(subscription?.currentBillingCycleEnd).format('MMMM D, YYYY')
|
||||
: 'Never'
|
||||
}}
|
||||
</h4>
|
||||
<p v-if="isPaidPlan" class="text-body-xs text-foreground-2">
|
||||
<span class="capitalize">{{ subscription?.billingInterval }}</span>
|
||||
billing period
|
||||
</p>
|
||||
</CommonCard>
|
||||
</div>
|
||||
|
||||
<SettingsSectionHeader
|
||||
title="What your workspace bill on a team plan will look like"
|
||||
class="pt-6 pb-4 md:pt-10 md:pb-6"
|
||||
subheading
|
||||
/>
|
||||
<BillingSummary v-if="billing?.cost" :workspace-cost="billing.cost" />
|
||||
<div
|
||||
v-if="discount && billing?.cost?.subTotal"
|
||||
class="flex mt-6 bg-foundation border-dashed border border-success"
|
||||
>
|
||||
<p class="flex-1 p-3">{{ discount.name }}</p>
|
||||
<p class="w-32 md:w-40 ml-4 p-3">
|
||||
£{{ billing.cost.subTotal * discount.amount }} / month
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 mt-2 flex flex-col md:flex-row md:items-center">
|
||||
<p class="text-body-xs text-foreground flex-1">
|
||||
To learn more about our pricing plans
|
||||
</p>
|
||||
<div class="pt-4 md:pt-0 md:pl-4 md:w-40">
|
||||
<FormButton
|
||||
:external="true"
|
||||
to="mailto:hello@speckle.systems"
|
||||
color="primary"
|
||||
>
|
||||
Talk to us
|
||||
</FormButton>
|
||||
<CommonCard v-if="isActivePlan" class="bg-foundation">
|
||||
<div class="flex flex-row gap-x-4 items-center">
|
||||
<p class="text-body-xs text-foreground-2 flex-1">
|
||||
View invoices, edit payment details, and manage your subscription from
|
||||
the billing portal
|
||||
</p>
|
||||
<FormButton
|
||||
color="outline"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
@click="openCustomerPortal"
|
||||
>
|
||||
Open billing portal
|
||||
</FormButton>
|
||||
</div>
|
||||
</CommonCard>
|
||||
|
||||
<SettingsSectionHeader title="Price plans" subheading class="pt-4" />
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="flex-col pr-6 gap-y-1">
|
||||
<p class="text-body-xs font-medium text-foreground">Annual billing</p>
|
||||
<p class="text-body-xs text-foreground-2 leading-5 max-w-md">
|
||||
Choose annual billing for a 20% discount
|
||||
</p>
|
||||
</div>
|
||||
<FormSwitch
|
||||
v-model="isYearlyPlan"
|
||||
:show-label="false"
|
||||
name="annual billing"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<CommonCard
|
||||
v-for="pricingPlan in pricingPlans"
|
||||
:key="pricingPlan.name"
|
||||
class="gap-y-4"
|
||||
>
|
||||
<h4 class="text-heading text-foreground capitalize">
|
||||
{{ pricingPlan.name }}
|
||||
</h4>
|
||||
<FormButton
|
||||
color="outline"
|
||||
full-width
|
||||
@click="onUpgradePlanClick(pricingPlan.name)"
|
||||
>
|
||||
Upgrade
|
||||
</FormButton>
|
||||
</CommonCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>Coming soon</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { settingsWorkspaceBillingQuery } from '~/lib/settings/graphql/queries'
|
||||
import { useQuery, useApolloClient } from '@vue/apollo-composable'
|
||||
import {
|
||||
settingsWorkspaceBillingQuery,
|
||||
settingsWorkspacePricingPlansQuery,
|
||||
settingsWorkspaceBillingCustomerPortalQuery
|
||||
} from '~/lib/settings/graphql/queries'
|
||||
import { useIsBillingIntegrationEnabled } from '~/composables/globals'
|
||||
import {
|
||||
WorkspacePlans,
|
||||
WorkspacePlanStatuses,
|
||||
BillingInterval
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/outline'
|
||||
import { isWorkspacePricingPlans } from '~/lib/settings/helpers/types'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesBilling_Workspace on Workspace {
|
||||
billing {
|
||||
cost {
|
||||
subTotal
|
||||
total
|
||||
...BillingSummary_WorkspaceCost
|
||||
}
|
||||
versionsCount {
|
||||
current
|
||||
max
|
||||
}
|
||||
id
|
||||
plan {
|
||||
name
|
||||
status
|
||||
}
|
||||
subscription {
|
||||
billingInterval
|
||||
currentBillingCycleEnd
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
type SeatPrices = {
|
||||
[key in WorkspacePlans]: {
|
||||
[BillingInterval.Monthly]: number
|
||||
[BillingInterval.Yearly]: number
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
workspaceId: string
|
||||
}>()
|
||||
|
||||
const { result } = useQuery(settingsWorkspaceBillingQuery, () => ({
|
||||
const isBillingIntegrationEnabled = useIsBillingIntegrationEnabled()
|
||||
const isYearlyPlan = ref(false)
|
||||
// TODO: get these from the backend when available
|
||||
const seatPrices = ref<SeatPrices>({
|
||||
[WorkspacePlans.Team]: { monthly: 12, yearly: 10 },
|
||||
[WorkspacePlans.Pro]: { monthly: 40, yearly: 36 },
|
||||
[WorkspacePlans.Business]: { monthly: 79, yearly: 63 },
|
||||
[WorkspacePlans.Academia]: { monthly: 0, yearly: 0 },
|
||||
[WorkspacePlans.Unlimited]: { monthly: 0, yearly: 0 }
|
||||
})
|
||||
|
||||
const { client: apollo } = useApolloClient()
|
||||
const { result: workspaceResult } = useQuery(settingsWorkspaceBillingQuery, () => ({
|
||||
workspaceId: props.workspaceId
|
||||
}))
|
||||
const { result: pricingPlansResult } = useQuery(settingsWorkspacePricingPlansQuery)
|
||||
|
||||
const billing = computed(() => result.value?.workspace.billing)
|
||||
const versionCount = computed(() => billing.value?.versionsCount)
|
||||
const discount = computed(() => billing.value?.cost?.discount)
|
||||
const currentPlan = computed(() => workspaceResult.value?.workspace.plan)
|
||||
const subscription = computed(() => workspaceResult.value?.workspace.subscription)
|
||||
const isPaidPlan = computed(
|
||||
() =>
|
||||
currentPlan.value?.name !== WorkspacePlans.Academia &&
|
||||
currentPlan.value?.name !== WorkspacePlans.Unlimited
|
||||
)
|
||||
const isTrialPeriod = computed(
|
||||
() => currentPlan.value?.status === WorkspacePlanStatuses.Trial
|
||||
)
|
||||
const isActivePlan = computed(
|
||||
() =>
|
||||
currentPlan.value?.status !== WorkspacePlanStatuses.Trial &&
|
||||
currentPlan.value?.status !== WorkspacePlanStatuses.Canceled
|
||||
)
|
||||
const seatPrice = computed(() =>
|
||||
currentPlan.value && subscription.value
|
||||
? seatPrices.value[currentPlan.value?.name][subscription.value?.billingInterval]
|
||||
: 0
|
||||
)
|
||||
const pricingPlans = computed(() =>
|
||||
isWorkspacePricingPlans(pricingPlansResult.value)
|
||||
? pricingPlansResult.value?.workspacePricingPlans.workspacePlanInformation
|
||||
: undefined
|
||||
)
|
||||
|
||||
const onUpgradePlanClick = (plan: WorkspacePlans) => {
|
||||
const cycle = isYearlyPlan.value ? BillingInterval.Yearly : BillingInterval.Monthly
|
||||
window.location.href = `/api/v1/billing/workspaces/${props.workspaceId}/checkout-session/${plan}/${cycle}`
|
||||
}
|
||||
|
||||
const openCustomerPortal = async () => {
|
||||
// We need to fetch this on click because the link expires very quickly
|
||||
const result = await apollo.query({
|
||||
query: settingsWorkspaceBillingCustomerPortalQuery,
|
||||
variables: { workspaceId: props.workspaceId },
|
||||
fetchPolicy: 'no-cache'
|
||||
})
|
||||
|
||||
if (result.data?.workspace.customerPortalUrl) {
|
||||
window.location.href = result.data.workspace.customerPortalUrl
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -290,6 +290,8 @@ const updateWorkspaceSlug = async (newSlug: string) => {
|
|||
title: 'Workspace short ID updated'
|
||||
})
|
||||
|
||||
showEditSlugDialog.value = false
|
||||
|
||||
slug.value = newSlug
|
||||
|
||||
if (route.params.slug === oldSlug) {
|
||||
|
|
|
@ -5,23 +5,36 @@
|
|||
max-width="sm"
|
||||
:buttons="dialogButtons"
|
||||
>
|
||||
<p class="text-body-xs text-foreground">
|
||||
Are you sure you want to
|
||||
<span class="font-medium">permanently delete</span>
|
||||
the selected workspace?
|
||||
</p>
|
||||
<div
|
||||
class="rounded border bg-foundation-2 border-outline-3 text-body-2xs text-foreground font-medium py-3 px-4 my-4"
|
||||
>
|
||||
{{ workspace.name }}
|
||||
</div>
|
||||
<p class="text-body-xs text-foreground">
|
||||
This action
|
||||
<span class="font-medium">cannot</span>
|
||||
be undone.
|
||||
<p class="text-body-xs text-foreground mb-2">
|
||||
Are you sure you want to permanently delete
|
||||
<span class="font-medium">{{ workspace.name }}?</span>
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<FormTextInput
|
||||
v-model="workspaceNameInput"
|
||||
name="workspaceNameConfirm"
|
||||
label="To confirm deletion, type the workspace name below."
|
||||
placeholder="Type the workspace name here..."
|
||||
full-width
|
||||
show-label
|
||||
hide-error-message
|
||||
class="text-sm mb-2"
|
||||
color="foundation"
|
||||
/>
|
||||
<FormTextArea
|
||||
v-model="feedback"
|
||||
name="reasonForDeletion"
|
||||
label="Why did you delete this workspace?"
|
||||
placeholder="We want to improve so we're curious about your honest feedback"
|
||||
show-label
|
||||
show-optional
|
||||
full-width
|
||||
class="text-sm mb-2"
|
||||
color="foundation"
|
||||
/>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type {
|
||||
|
@ -29,7 +42,7 @@ import type {
|
|||
UserWorkspacesArgs,
|
||||
User
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { FormTextInput, type LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { useMutation, useApolloClient } from '@vue/apollo-composable'
|
||||
import { deleteWorkspaceMutation } from '~/lib/settings/graphql/mutations'
|
||||
import {
|
||||
|
@ -43,6 +56,8 @@ import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
|||
import { isUndefined } from 'lodash-es'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { homeRoute } from '~/lib/common/helpers/route'
|
||||
import { useZapier } from '~/lib/core/composables/zapier'
|
||||
import { useForm } from 'vee-validate'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspaceGeneralDeleteDialog_Workspace on Workspace {
|
||||
|
@ -63,10 +78,14 @@ const { activeUser } = useActiveUser()
|
|||
const router = useRouter()
|
||||
const apollo = useApolloClient().client
|
||||
const mixpanel = useMixpanel()
|
||||
const { sendWebhook } = useZapier()
|
||||
const { resetForm } = useForm<{ feedback: string }>()
|
||||
|
||||
const workspaceNameInput = ref('')
|
||||
const feedback = ref('')
|
||||
|
||||
const onDelete = async () => {
|
||||
router.push(homeRoute)
|
||||
isOpen.value = false
|
||||
if (workspaceNameInput.value !== props.workspace.name) return
|
||||
|
||||
const cache = apollo.cache
|
||||
const result = await deleteWorkspace({
|
||||
|
@ -98,16 +117,30 @@ const onDelete = async () => {
|
|||
)
|
||||
}
|
||||
|
||||
mixpanel.track('Workspace Deleted', {
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: props.workspace.id,
|
||||
feedback: feedback.value
|
||||
})
|
||||
|
||||
// Only send zapier-discord webhook if not in dev environment
|
||||
if (!import.meta.dev) {
|
||||
await sendWebhook('https://hooks.zapier.com/hooks/catch/12120532/2m4okri/', {
|
||||
userId: activeUser.value?.id ?? '',
|
||||
feedback: feedback.value
|
||||
? `Action: Workspace Deleted(${props.workspace.name}) Feedback: ${feedback.value}`
|
||||
: `Action: Workspace Deleted(${props.workspace.name}) - No feedback provided`
|
||||
})
|
||||
}
|
||||
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Success,
|
||||
title: 'Workspace deleted',
|
||||
description: `The ${props.workspace.name} workspace has been deleted`
|
||||
})
|
||||
|
||||
mixpanel.track('Workspace Deleted', {
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: props.workspace.id
|
||||
})
|
||||
router.push(homeRoute)
|
||||
isOpen.value = false
|
||||
} else {
|
||||
const errorMessage = getFirstErrorMessage(result?.errors)
|
||||
triggerNotification({
|
||||
|
@ -129,9 +162,14 @@ const dialogButtons = computed((): LayoutDialogButton[] => [
|
|||
{
|
||||
text: 'Delete',
|
||||
props: {
|
||||
color: 'danger'
|
||||
color: 'danger',
|
||||
disabled: workspaceNameInput.value !== props.workspace.name
|
||||
},
|
||||
onClick: onDelete
|
||||
}
|
||||
])
|
||||
|
||||
watch(isOpen, () => {
|
||||
resetForm()
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -19,11 +19,16 @@
|
|||
<FormTextInput
|
||||
v-model:model-value="workspaceShortId"
|
||||
name="slug"
|
||||
label="Short ID"
|
||||
label="New short ID"
|
||||
:help="`${baseUrl}${workspaceRoute(workspaceShortId)}`"
|
||||
color="foundation"
|
||||
:rules="[isStringOfLength({ maxLength: 50, minLength: 3 }), isValidWorkspaceSlug]"
|
||||
:rules="[isStringOfLength({ maxLength: 50, minLength: 3 })]"
|
||||
:custom-error-message="
|
||||
workspaceShortId !== originalSlug ? error?.graphQLErrors[0]?.message : undefined
|
||||
"
|
||||
:loading="loading"
|
||||
show-label
|
||||
@update:model-value="updateDebouncedShortId"
|
||||
/>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
@ -33,11 +38,11 @@ import { useForm } from 'vee-validate'
|
|||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import type { SettingsWorkspacesGeneralEditSlugDialog_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
isStringOfLength,
|
||||
isValidWorkspaceSlug
|
||||
} from '~~/lib/common/helpers/validation'
|
||||
import { isStringOfLength } from '~~/lib/common/helpers/validation'
|
||||
import { workspaceRoute } from '~/lib/common/helpers/route'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { validateWorkspaceSlugQuery } from '~/lib/workspaces/graphql/queries'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesGeneralEditSlugDialog_Workspace on Workspace {
|
||||
|
@ -57,13 +62,27 @@ const emit = defineEmits<{
|
|||
(e: 'update:slug', newSlug: string): void
|
||||
}>()
|
||||
|
||||
const { handleSubmit } = useForm<{ slug: string }>()
|
||||
|
||||
// Main ref that holds the current value of the slug input.
|
||||
const workspaceShortId = ref(props.workspace.slug)
|
||||
// Used to debounce API calls for slug validation.
|
||||
const debouncedWorkspaceShortId = ref(props.workspace.slug)
|
||||
// Keeps track of the initially generated slug to prevent unnecessary validations.
|
||||
const originalSlug = ref(props.workspace.slug)
|
||||
|
||||
const { error, loading } = useQuery(
|
||||
validateWorkspaceSlugQuery,
|
||||
() => ({
|
||||
slug: debouncedWorkspaceShortId.value
|
||||
}),
|
||||
() => ({
|
||||
enabled: debouncedWorkspaceShortId.value !== props.workspace.slug
|
||||
})
|
||||
)
|
||||
|
||||
const { handleSubmit, resetForm } = useForm<{ slug: string }>()
|
||||
|
||||
const updateSlug = handleSubmit(() => {
|
||||
emit('update:slug', workspaceShortId.value)
|
||||
isOpen.value = false
|
||||
})
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
|
@ -78,12 +97,16 @@ const dialogButtons = computed((): LayoutDialogButton[] => [
|
|||
text: 'Update',
|
||||
props: {
|
||||
color: 'primary',
|
||||
disabled: workspaceShortId.value === props.workspace.slug
|
||||
disabled: workspaceShortId.value === props.workspace.slug || error.value !== null
|
||||
},
|
||||
submit: true
|
||||
}
|
||||
])
|
||||
|
||||
const updateDebouncedShortId = debounce((value: string) => {
|
||||
debouncedWorkspaceShortId.value = value
|
||||
}, 300)
|
||||
|
||||
watch(
|
||||
() => props.workspace.slug,
|
||||
(newValue) => {
|
||||
|
@ -91,4 +114,14 @@ watch(
|
|||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => isOpen.value,
|
||||
(newValue) => {
|
||||
if (!newValue) {
|
||||
resetForm()
|
||||
error.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<section>
|
||||
<div class="md:max-w-xl md:mx-auto pb-6 md:pb-0">
|
||||
<SettingsSectionHeader title="Regions" text="Manage your data residency" />
|
||||
<SettingsSectionHeader
|
||||
title="Default region"
|
||||
text="The default region will be used to store project data for new projects"
|
||||
subheading
|
||||
/>
|
||||
<div class="pt-6">
|
||||
<SettingsWorkspacesRegionsSelect
|
||||
v-model="defaultRegion"
|
||||
show-label
|
||||
label="Default region"
|
||||
:items="availableRegions || []"
|
||||
:disabled="!availableRegions?.length || isLoading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useMutationLoading, useQuery } from '@vue/apollo-composable'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { SettingsWorkspacesRegionsSelect_ServerRegionItemFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { settingsWorkspaceRegionsQuery } from '~/lib/settings/graphql/queries'
|
||||
import { useSetDefaultWorkspaceRegion } from '~/lib/workspaces/composables/management'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesRegions_Workspace on Workspace {
|
||||
id
|
||||
defaultRegion {
|
||||
id
|
||||
...SettingsWorkspacesRegionsSelect_ServerRegionItem
|
||||
}
|
||||
availableRegions {
|
||||
id
|
||||
...SettingsWorkspacesRegionsSelect_ServerRegionItem
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
workspaceId: string
|
||||
}>()
|
||||
|
||||
const isLoading = useMutationLoading()
|
||||
const setDefaultWorkspaceRegion = useSetDefaultWorkspaceRegion()
|
||||
const { result } = useQuery(settingsWorkspaceRegionsQuery, () => ({
|
||||
workspaceId: props.workspaceId
|
||||
}))
|
||||
|
||||
const defaultRegion = ref<SettingsWorkspacesRegionsSelect_ServerRegionItemFragment>()
|
||||
const availableRegions = computed(() => result.value?.workspace.availableRegions || [])
|
||||
|
||||
const saveDefaultRegion = async () => {
|
||||
const regionKey = defaultRegion.value?.key
|
||||
if (!regionKey) return
|
||||
if (regionKey === result.value?.workspace.defaultRegion?.key) return
|
||||
|
||||
await setDefaultWorkspaceRegion({
|
||||
workspaceId: props.workspaceId,
|
||||
regionKey
|
||||
})
|
||||
}
|
||||
|
||||
const debouncedSaveDefaultRegion = debounce(saveDefaultRegion, 1000)
|
||||
|
||||
watch(
|
||||
result,
|
||||
() => {
|
||||
defaultRegion.value = result.value?.workspace.defaultRegion || undefined
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(defaultRegion, (newVal, oldVal) => {
|
||||
if (newVal === oldVal) return
|
||||
if (newVal?.id === oldVal?.id) return
|
||||
debouncedSaveDefaultRegion()
|
||||
})
|
||||
</script>
|
|
@ -84,6 +84,15 @@
|
|||
:user="userToModify"
|
||||
:workspace-id="workspaceId"
|
||||
/>
|
||||
|
||||
<SettingsSharedChangeRoleDialog
|
||||
v-model:open="showChangeUserRoleDialog"
|
||||
:workspace-domain-policy-compliant="
|
||||
userToModify?.user.workspaceDomainPolicyCompliant
|
||||
"
|
||||
:current-role="Roles.Workspace.Guest"
|
||||
@update-role="onUpdateRole"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -93,10 +102,9 @@ import type {
|
|||
WorkspaceCollaborator
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { Roles, type WorkspaceRoles } from '@speckle/shared'
|
||||
import { settingsWorkspacesMembersSearchQuery } from '~~/lib/settings/graphql/queries'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { useWorkspaceUpdateRole } from '~/lib/workspaces/composables/management'
|
||||
import { HorizontalDirection } from '~~/lib/common/composables/window'
|
||||
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
|
||||
|
@ -138,7 +146,8 @@ graphql(`
|
|||
|
||||
enum ActionTypes {
|
||||
ChangeProjectPermissions = 'change-project-permissions',
|
||||
RemoveMember = 'remove-member'
|
||||
RemoveMember = 'remove-member',
|
||||
ChangeRole = 'change-role'
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -151,12 +160,12 @@ const showActionsMenu = ref<Record<string, boolean>>({})
|
|||
const showDeleteUserRoleDialog = ref(false)
|
||||
const showGuestsPermissionsDialog = ref(false)
|
||||
const userIdToModify = ref<string | null>(null)
|
||||
const showChangeUserRoleDialog = ref(false)
|
||||
|
||||
const userToModify = computed(
|
||||
() => guests.value.find((guest) => guest.id === userIdToModify.value) || null
|
||||
)
|
||||
|
||||
const mixpanel = useMixpanel()
|
||||
const updateUserRole = useWorkspaceUpdateRole()
|
||||
|
||||
const { result: searchResult, loading: searchResultLoading } = useQuery(
|
||||
|
@ -190,6 +199,10 @@ const actionItems = computed(() => {
|
|||
[{ title: 'Remove guest...', id: ActionTypes.RemoveMember }]
|
||||
]
|
||||
|
||||
if (isWorkspaceAdmin.value) {
|
||||
items.unshift([{ title: 'Update role...', id: ActionTypes.ChangeRole }])
|
||||
}
|
||||
|
||||
if (guests.value.find((guest) => guest.projectRoles.length)) {
|
||||
items.unshift([
|
||||
{
|
||||
|
@ -208,6 +221,9 @@ const onActionChosen = (actionItem: LayoutMenuItem, user: WorkspaceCollaborator)
|
|||
if (actionItem.id === ActionTypes.ChangeProjectPermissions) {
|
||||
showGuestsPermissionsDialog.value = true
|
||||
}
|
||||
if (actionItem.id === ActionTypes.ChangeRole) {
|
||||
showChangeUserRoleDialog.value = true
|
||||
}
|
||||
if (actionItem.id === ActionTypes.RemoveMember) {
|
||||
showDeleteUserRoleDialog.value = true
|
||||
}
|
||||
|
@ -221,14 +237,19 @@ const onRemoveUser = async () => {
|
|||
role: null,
|
||||
workspaceId: props.workspaceId
|
||||
})
|
||||
|
||||
mixpanel.track('Workspace User Removed', {
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: props.workspaceId
|
||||
})
|
||||
}
|
||||
|
||||
const toggleMenu = (itemId: string) => {
|
||||
showActionsMenu.value[itemId] = !showActionsMenu.value[itemId]
|
||||
}
|
||||
|
||||
const onUpdateRole = async (newRoleValue: WorkspaceRoles) => {
|
||||
if (!userToModify.value || !newRoleValue) return
|
||||
|
||||
await updateUserRole({
|
||||
userId: userToModify.value.id,
|
||||
role: newRoleValue,
|
||||
workspaceId: props.workspaceId
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -82,9 +82,8 @@
|
|||
</LayoutTable>
|
||||
<SettingsSharedChangeRoleDialog
|
||||
v-model:open="showChangeUserRoleDialog"
|
||||
:name="userToModify?.name ?? ''"
|
||||
:is-workspace-admin="isWorkspaceAdmin"
|
||||
:workspace-domain-policy-compliant="userToModify?.workspaceDomainPolicyCompliant"
|
||||
:current-role="currentUserRole"
|
||||
@update-role="onUpdateRole"
|
||||
/>
|
||||
<SettingsSharedDeleteUserDialog
|
||||
|
@ -117,7 +116,6 @@ import {
|
|||
import { useWorkspaceUpdateRole } from '~/lib/workspaces/composables/management'
|
||||
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
|
||||
import { HorizontalDirection } from '~~/lib/common/composables/window'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { getRoleLabel } from '~~/lib/settings/helpers/utils'
|
||||
|
||||
type UserItem = (typeof members)['value'][0]
|
||||
|
@ -182,7 +180,6 @@ const { result: searchResult, loading: searchResultLoading } = useQuery(
|
|||
)
|
||||
|
||||
const updateUserRole = useWorkspaceUpdateRole()
|
||||
const mixpanel = useMixpanel()
|
||||
const { activeUser } = useActiveUser()
|
||||
|
||||
const showChangeUserRoleDialog = ref(false)
|
||||
|
@ -218,6 +215,14 @@ const hasNoResults = computed(
|
|||
(search.value.length || roleFilter.value) &&
|
||||
searchResult.value?.workspace.team.items.length === 0
|
||||
)
|
||||
|
||||
const currentUserRole = computed<WorkspaceRoles | undefined>(() => {
|
||||
if (userToModify.value?.role && isWorkspaceRole(userToModify.value.role)) {
|
||||
return userToModify.value.role
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const filteredActionsItems = (user: UserItem) => {
|
||||
const baseItems: LayoutMenuItem[][] = []
|
||||
|
||||
|
@ -262,12 +267,6 @@ const onUpdateRole = async (newRoleValue: WorkspaceRoles) => {
|
|||
role: newRoleValue,
|
||||
workspaceId: props.workspaceId
|
||||
})
|
||||
|
||||
mixpanel.track('Workspace User Role Updated', {
|
||||
newRole: newRoleValue,
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: props.workspaceId
|
||||
})
|
||||
}
|
||||
|
||||
const onRemoveUser = async () => {
|
||||
|
@ -278,11 +277,6 @@ const onRemoveUser = async () => {
|
|||
role: null,
|
||||
workspaceId: props.workspaceId
|
||||
})
|
||||
|
||||
mixpanel.track('Workspace User Removed', {
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: props.workspaceId
|
||||
})
|
||||
}
|
||||
|
||||
const onActionChosen = (actionItem: LayoutMenuItem, user: UserItem) => {
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<FormSelectBase
|
||||
v-bind="props"
|
||||
v-model="selectedValue"
|
||||
:name="name || 'regions'"
|
||||
:allow-unset="false"
|
||||
mount-menu-on-body
|
||||
>
|
||||
<template #option="{ item }">
|
||||
<div class="flex flex-col items-start justify-center">
|
||||
<span class="truncate">{{ item.name }}</span>
|
||||
<span class="text-foreground-2 truncate">{{ item.description }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #nothing-selected>
|
||||
{{ multiple ? 'Select regions' : 'Select a region' }}
|
||||
</template>
|
||||
<template #something-selected="{ value }">
|
||||
<template v-if="isArray(value)">
|
||||
{{ value.map((v) => v.name).join(', ') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ value.name }}
|
||||
</template>
|
||||
</template>
|
||||
</FormSelectBase>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { isArray } from 'lodash-es'
|
||||
import type { RuleExpression } from 'vee-validate'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { SettingsWorkspacesRegionsSelect_ServerRegionItemFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { useFormSelectChildInternals } from '~/lib/form/composables/select'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesRegionsSelect_ServerRegionItem on ServerRegionItem {
|
||||
id
|
||||
key
|
||||
name
|
||||
description
|
||||
}
|
||||
`)
|
||||
|
||||
type ItemType = SettingsWorkspacesRegionsSelect_ServerRegionItemFragment
|
||||
type ValueType = ItemType | ItemType[] | undefined
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: ValueType): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: ValueType
|
||||
label: string
|
||||
items: ItemType[]
|
||||
multiple?: boolean
|
||||
name?: string
|
||||
showOptional?: boolean
|
||||
showRequired?: boolean
|
||||
showLabel?: boolean
|
||||
labelId?: string
|
||||
buttonId?: string
|
||||
help?: string
|
||||
disabled?: boolean
|
||||
rules?: RuleExpression<ItemType | ItemType[] | undefined>
|
||||
}>()
|
||||
|
||||
const { selectedValue } = useFormSelectChildInternals<ItemType>({
|
||||
props: toRefs(props),
|
||||
emit
|
||||
})
|
||||
</script>
|
|
@ -51,7 +51,9 @@
|
|||
:rules="[isRequired, isUrl, isStringOfLength({ minLength: 5 })]"
|
||||
/>
|
||||
<div class="mt-6">
|
||||
<FormButton color="primary" @click="onSubmit">Save</FormButton>
|
||||
<FormButton :disabled="!challenge" color="primary" @click="onSubmit">
|
||||
Save
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -63,6 +65,8 @@ import { useForm } from 'vee-validate'
|
|||
import { isRequired, isStringOfLength, isUrl } from '~~/lib/common/helpers/validation'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type { SettingsWorkspacesSecuritySso_WorkspaceFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
import { usePostAuthRedirect } from '~/lib/auth/composables/postAuthRedirect'
|
||||
import { useLoginOrRegisterUtils } from '~/lib/auth/composables/auth'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesSecuritySso_Workspace on Workspace {
|
||||
|
@ -83,6 +87,8 @@ const props = defineProps<{
|
|||
}>()
|
||||
|
||||
const apiOrigin = useApiOrigin()
|
||||
const { challenge } = useLoginOrRegisterUtils()
|
||||
const postAuthRedirect = usePostAuthRedirect()
|
||||
const { handleSubmit } = useForm<FormValues>()
|
||||
|
||||
const providerName = ref('')
|
||||
|
@ -96,10 +102,13 @@ const onSubmit = handleSubmit(() => {
|
|||
`providerName=${providerName.value}`,
|
||||
`clientId=${clientId.value}`,
|
||||
`clientSecret=${clientSecret.value}`,
|
||||
`issuerUrl=${issuerUrl.value}`
|
||||
`issuerUrl=${issuerUrl.value}`,
|
||||
`challenge=${challenge.value}`
|
||||
]
|
||||
const route = `${baseUrl}?${params.join('&')}`
|
||||
|
||||
postAuthRedirect.set(`/workspaces/${props.workspace.slug}?settings=server/general`)
|
||||
|
||||
navigateTo(route, {
|
||||
external: true
|
||||
})
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
>
|
||||
<div
|
||||
v-show="item.expanded"
|
||||
class="transition bg-foundation rounded-lg shadow-md mb-8 mx-2 px-4 py-4 gap-2 sm:gap-4 sm:ml-12 sm:max-w-xs pointer-events-auto"
|
||||
class="transition bg-foundation-page border border-outline-3 rounded-lg shadow-md mb-8 mx-2 gap-2 sm:gap-4 sm:ml-12 sm:max-w-xs pointer-events-auto"
|
||||
>
|
||||
<div
|
||||
class="sm:hidden flex items-center justify-center w-full gap-3 mt-1 mb-3"
|
||||
|
@ -41,21 +41,35 @@
|
|||
></div>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
<div class="px-6 py-4">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pointer-events-auto mt-4">
|
||||
<div
|
||||
class="flex items-center justify-between pointer-events-auto px-6 py-2 border-t border-outline-3"
|
||||
>
|
||||
<slot name="actions">
|
||||
<FormButton text color="outline" @click="$emit('skip')">Skip</FormButton>
|
||||
<div class="flex justify-center space-x-2">
|
||||
<FormButton text size="sm" color="outline" @click="$emit('skip')">
|
||||
Skip
|
||||
</FormButton>
|
||||
<div class="flex justify-center items-center space-x-2">
|
||||
<FormButton
|
||||
v-show="index !== 0"
|
||||
:icon-left="ArrowLeftIcon"
|
||||
size="sm"
|
||||
color="outline"
|
||||
text
|
||||
@click="prev(index)"
|
||||
>
|
||||
<ArrowLeftIcon class="h-3 w-3 mr-1" />
|
||||
Previous
|
||||
</FormButton>
|
||||
<FormButton :icon-right="ArrowRightIcon" @click="next(index)">
|
||||
<div v-if="index === 2">
|
||||
<div v-if="!disableNext" v-tippy="'First add another model'">
|
||||
<FormButton disabled>Finish</FormButton>
|
||||
</div>
|
||||
<FormButton v-else @click="$emit('skip')">Finish</FormButton>
|
||||
</div>
|
||||
<FormButton v-else :icon-right="ArrowRightIcon" @click="next(index)">
|
||||
Next
|
||||
</FormButton>
|
||||
</div>
|
||||
|
@ -83,6 +97,7 @@ defineEmits(['skip', 'previous', 'next'])
|
|||
const props = defineProps<{
|
||||
index: number
|
||||
item: SlideshowItem
|
||||
disableNext: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
|
|
|
@ -2,48 +2,24 @@
|
|||
<div
|
||||
class="relative max-w-4xl w-screen h-[100dvh] flex items-center justify-center z-50"
|
||||
>
|
||||
<TourSegmentation v-if="showSegmentation && step === 0" @next="step++" />
|
||||
<TourSlideshow v-if="step === 1" @next="step++" />
|
||||
<!-- <OnboardingDialogManager v-if="step === 2" allow-escape @cancel="step++" /> -->
|
||||
<div
|
||||
v-if="step === 2 && !hasCompletedChecklistV1"
|
||||
class="relative w-full pointer-events-auto space-y-2 z-50"
|
||||
>
|
||||
<div
|
||||
v-if="!isSmallerOrEqualSm"
|
||||
class="pointer-events-none fixed inset-0 bg-neutral-100/70 dark:bg-neutral-900/70 transition-opacity z-50"
|
||||
/>
|
||||
<div v-if="!isSmallerOrEqualSm" class="relative z-50">
|
||||
<OnboardingChecklistV1 show-bottom-escape background @dismiss="step++" />
|
||||
</div>
|
||||
</div>
|
||||
<TourSegmentation v-if="showSegmentation" />
|
||||
<TourSlideshow v-else @next="$emit('complete')" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useViewerTour } from '~/lib/viewer/composables/tour'
|
||||
import { useSynchronizedCookie } from '~~/lib/common/composables/reactiveCookie'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
|
||||
const { isSmallerOrEqualSm } = useIsSmallerOrEqualThanBreakpoint()
|
||||
|
||||
const step = ref(0)
|
||||
const hasCompletedChecklistV1 = useSynchronizedCookie<boolean>(
|
||||
`hasCompletedChecklistV1`,
|
||||
{ default: () => false }
|
||||
)
|
||||
defineEmits(['complete'])
|
||||
|
||||
const { showSegmentation } = useViewerTour()
|
||||
|
||||
const mp = useMixpanel()
|
||||
watch(step, (val) => {
|
||||
let stepName = 'segmentation'
|
||||
if (val === 1) stepName = 'slideshow'
|
||||
if (val === 2) stepName = 'checklist'
|
||||
watch(showSegmentation, (val) => {
|
||||
mp.track('Onboarding Action', {
|
||||
type: 'action',
|
||||
name: 'step-activation',
|
||||
step: val,
|
||||
stepName
|
||||
stepName: val ? 'slideshow' : 'segmentation'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -15,9 +15,10 @@
|
|||
:class="isSmallerOrEqualSm ? 'bottom-0 left-0 w-screen' : ''"
|
||||
:style="isSmallerOrEqualSm ? undefined : item.style"
|
||||
:show-controls="item.showControls"
|
||||
:disable-next="hasAddedOverlay"
|
||||
@skip="finishSlideshow()"
|
||||
>
|
||||
<Component :is="tourItems[index]" />
|
||||
<Component :is="tourItems[index]" @has-added-overlay="hasAddedOverlay = true" />
|
||||
</TourComment>
|
||||
<!-- In case the bubble is closed by the user, we need to display something -->
|
||||
<Transition
|
||||
|
@ -76,6 +77,7 @@ const tourItems = [FirstTip, BasicViewerNavigation, OverlayModel /* , LastTip */
|
|||
const slideshowItems = ref(slideshowItemsRaw.slice(0, tourItems.length))
|
||||
provide('slideshowItems', slideshowItems)
|
||||
|
||||
const hasAddedOverlay = ref(false)
|
||||
const lastOpenIndex = ref(0)
|
||||
const mp = useMixpanel()
|
||||
|
||||
|
|
|
@ -4,18 +4,16 @@
|
|||
<p class="text-sm">
|
||||
Speckle allows you to load multiple models in the same viewer.
|
||||
</p>
|
||||
<p class="text-sm mt-2">
|
||||
<p class="text-sm mt-3">
|
||||
<span v-show="!hasAddedOverlay">
|
||||
<FormButton
|
||||
link
|
||||
text
|
||||
:icon-right="hasAddedOverlay ? CheckIcon : null"
|
||||
color="outline"
|
||||
:icon-right="hasAddedOverlay ? CheckIcon : PlusIcon"
|
||||
:disabled="hasAddedOverlay"
|
||||
@click="addOverlay()"
|
||||
>
|
||||
Click here
|
||||
Add another model
|
||||
</FormButton>
|
||||
to give it a try!
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -31,7 +29,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon } from '@heroicons/vue/24/solid'
|
||||
import { CheckIcon, PlusIcon } from '@heroicons/vue/24/solid'
|
||||
import { SpeckleViewer } from '@speckle/shared'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { latestModelsQuery } from '~~/lib/projects/graphql/queries'
|
||||
|
@ -42,6 +40,8 @@ import {
|
|||
|
||||
import { SECOND_MODEL_NAME } from '~~/lib/auth/composables/onboarding'
|
||||
|
||||
const emit = defineEmits(['hasAddedOverlay'])
|
||||
|
||||
const { items } = useInjectedViewerRequestedResources()
|
||||
const { project } = useInjectedViewerLoadedResources()
|
||||
const id = project.value?.id as string
|
||||
|
@ -62,6 +62,7 @@ async function addOverlay() {
|
|||
])
|
||||
|
||||
hasAddedOverlay.value = true
|
||||
emit('hasAddedOverlay')
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
|
|
@ -78,15 +78,11 @@
|
|||
(!isEmbedEnabled && spotlightUserSessionId && spotlightUser) ||
|
||||
followers.length !== 0
|
||||
"
|
||||
class="absolute w-screen z-10 p-1"
|
||||
:class="
|
||||
isEmbedEnabled
|
||||
? 'h-[calc(100dvh-3.5rem)]'
|
||||
: 'h-[calc(100dvh-3.5rem)] mt-[3.5rem]'
|
||||
"
|
||||
class="absolute w-screen z-10 p-1 h-[calc(100dvh-3rem)]"
|
||||
:class="isEmbedEnabled ? '' : 'mt-[3rem]'"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full outline -outline-offset-0 outline-8 rounded-md outline-blue-500"
|
||||
class="w-full h-full outline -outline-offset-0 outline-8 rounded-md outline-primary"
|
||||
>
|
||||
<div class="absolute top-0 left-0 w-full justify-center flex">
|
||||
<svg
|
||||
|
@ -96,7 +92,7 @@
|
|||
viewBox="0 0 8 8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M0 0C4.5 0 8 3.5 8 8V0H0Z" class="fill-blue-500" />
|
||||
<path d="M0 0C4.5 0 8 3.5 8 8V0H0Z" class="fill-primary" />
|
||||
</svg>
|
||||
<div
|
||||
class="pointer-events-auto bg-primary text-white text-xs px-3 h-8 flex items-center rounded-b-md cursor-default"
|
||||
|
@ -131,7 +127,7 @@
|
|||
viewBox="0 0 8 8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M0 0H8C3.5 0 0 3.5 0 8V0Z" class="fill-blue-500" />
|
||||
<path d="M0 0H8C3.5 0 0 3.5 0 8V0Z" class="fill-primary" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
v-tippy="isSmallerOrEqualSm ? undefined : sectionBoxShortcut"
|
||||
flat
|
||||
secondary
|
||||
:active="isSectionBoxEnabled"
|
||||
:active="isSectionBoxVisible"
|
||||
@click="toggleSectionBox()"
|
||||
>
|
||||
<ScissorsIcon class="h-4 w-4 md:h-5 md:w-5" />
|
||||
|
@ -238,6 +238,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Portal v-if="isSectionBoxEnabled && isSectionBoxEdited" to="pocket-actions">
|
||||
<FormButton @click="resetSectionBox()">Reset section box</FormButton>
|
||||
</Portal>
|
||||
</div>
|
||||
<div v-else />
|
||||
</template>
|
||||
|
@ -268,7 +271,6 @@ import {
|
|||
useInjectedViewerState
|
||||
} from '~~/lib/viewer/composables/setup'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
|
||||
import { useIsSmallerOrEqualThanBreakpoint } from '~~/composables/browser'
|
||||
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
|
||||
import { useViewerTour } from '~/lib/viewer/composables/tour'
|
||||
|
@ -346,7 +348,13 @@ type ActiveControl =
|
|||
| 'gendo'
|
||||
|
||||
const { resourceItems, modelsAndVersionIds } = useInjectedViewerLoadedResources()
|
||||
const { toggleSectionBox, isSectionBoxEnabled } = useSectionBoxUtilities()
|
||||
const {
|
||||
resetSectionBox,
|
||||
isSectionBoxEnabled,
|
||||
isSectionBoxVisible,
|
||||
toggleSectionBox,
|
||||
isSectionBoxEdited
|
||||
} = useSectionBoxUtilities()
|
||||
const { getActiveMeasurement, removeMeasurement, enableMeasurements } =
|
||||
useMeasurementUtilities()
|
||||
const { showNavbar, showControls } = useViewerTour()
|
||||
|
@ -388,12 +396,12 @@ const {
|
|||
} = useInjectedViewerInterfaceState()
|
||||
|
||||
const map: Record<ViewerKeyboardActions, [ModifierKeys[], string]> = {
|
||||
[ViewerKeyboardActions.ToggleModels]: [[ModifierKeys.Shift], 'm'],
|
||||
[ViewerKeyboardActions.ToggleExplorer]: [[ModifierKeys.Shift], 'e'],
|
||||
[ViewerKeyboardActions.ToggleDiscussions]: [[ModifierKeys.Shift], 't'],
|
||||
[ViewerKeyboardActions.ToggleMeasurements]: [[ModifierKeys.Shift], 'r'],
|
||||
[ViewerKeyboardActions.ToggleProjection]: [[ModifierKeys.Shift], 'p'],
|
||||
[ViewerKeyboardActions.ToggleSectionBox]: [[ModifierKeys.Shift], 'b'],
|
||||
[ViewerKeyboardActions.ToggleModels]: [[ModifierKeys.Shift], 'M'],
|
||||
[ViewerKeyboardActions.ToggleExplorer]: [[ModifierKeys.Shift], 'E'],
|
||||
[ViewerKeyboardActions.ToggleDiscussions]: [[ModifierKeys.Shift], 'T'],
|
||||
[ViewerKeyboardActions.ToggleMeasurements]: [[ModifierKeys.Shift], 'R'],
|
||||
[ViewerKeyboardActions.ToggleProjection]: [[ModifierKeys.Shift], 'P'],
|
||||
[ViewerKeyboardActions.ToggleSectionBox]: [[ModifierKeys.Shift], 'B'],
|
||||
[ViewerKeyboardActions.ZoomExtentsOrSelection]: [[ModifierKeys.Shift], 'space']
|
||||
}
|
||||
|
||||
|
@ -491,14 +499,6 @@ const trackAndtoggleProjection = () => {
|
|||
})
|
||||
}
|
||||
|
||||
watch(isSectionBoxEnabled, (val) => {
|
||||
mp.track('Viewer Action', {
|
||||
type: 'action',
|
||||
name: 'section-box',
|
||||
status: val
|
||||
})
|
||||
})
|
||||
|
||||
const scrollControlsToBottom = () => {
|
||||
// TODO: Currently this will scroll to the very bottom, which doesn't make sense when there are multiple models loaded
|
||||
// if (scrollableControlsContainer.value)
|
||||
|
@ -515,10 +515,6 @@ onMounted(() => {
|
|||
activeControl.value = isSmallerOrEqualSm.value ? 'none' : 'models'
|
||||
})
|
||||
|
||||
watch(isSmallerOrEqualSm, (newVal) => {
|
||||
activeControl.value = newVal ? 'none' : 'models'
|
||||
})
|
||||
|
||||
onKeyStroke('Escape', () => {
|
||||
const isActiveMeasurement = getActiveMeasurement()
|
||||
|
||||
|
@ -531,4 +527,24 @@ onKeyStroke('Escape', () => {
|
|||
activeControl.value = 'none'
|
||||
}
|
||||
})
|
||||
|
||||
watch(isSmallerOrEqualSm, (newVal) => {
|
||||
activeControl.value = newVal ? 'none' : 'models'
|
||||
})
|
||||
|
||||
watch(isSectionBoxEnabled, (val) => {
|
||||
mp.track('Viewer Action', {
|
||||
type: 'action',
|
||||
name: 'section-box',
|
||||
status: val
|
||||
})
|
||||
})
|
||||
|
||||
watch(isSectionBoxVisible, (val) => {
|
||||
mp.track('Viewer Action', {
|
||||
type: 'action',
|
||||
name: 'section-box-visibility',
|
||||
status: val
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
v-if="showTour"
|
||||
class="fixed w-full h-[100dvh] flex justify-center items-center pointer-events-none z-[100]"
|
||||
>
|
||||
<TourOnboarding />
|
||||
<TourOnboarding @complete="showTour = false" />
|
||||
</div>
|
||||
<!-- Viewer host -->
|
||||
<div
|
||||
|
|
|
@ -62,7 +62,10 @@
|
|||
<div v-if="commentThreads.length === 0" class="pb-4">
|
||||
<ProjectPageLatestItemsCommentsEmptyState
|
||||
small
|
||||
:show-button="canPostComment"
|
||||
:show-button="canPostComment && hasSelectedObjects"
|
||||
:text="
|
||||
hasSelectedObjects ? undefined : 'Select an object to start collaborating'
|
||||
"
|
||||
@new-discussion="onNewDiscussion"
|
||||
/>
|
||||
</div>
|
||||
|
@ -88,6 +91,7 @@ import {
|
|||
} from '~~/lib/viewer/composables/setup'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { useCheckViewerCommentingAccess } from '~~/lib/viewer/composables/commentManagement'
|
||||
import { useSelectionUtilities } from '~~/lib/viewer/composables/ui'
|
||||
|
||||
defineEmits(['close'])
|
||||
|
||||
|
@ -168,7 +172,12 @@ watch(includeArchived, (newVal) =>
|
|||
})
|
||||
)
|
||||
|
||||
const { objectIds: selectedObjectIds } = useSelectionUtilities()
|
||||
|
||||
const hasSelectedObjects = computed(() => selectedObjectIds.value.size > 0)
|
||||
|
||||
const onNewDiscussion = () => {
|
||||
if (!hasSelectedObjects.value) return
|
||||
newThreadEditor.value = true
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { isNullOrUndefined } from '@speckle/shared'
|
||||
import { CommonLoadingBar } from '@speckle/ui-components'
|
||||
import { useLazyQuery } from '@vue/apollo-composable'
|
||||
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
|
||||
|
@ -42,8 +43,7 @@ const kvps = computed(() => {
|
|||
const keys = Object.keys(obj)
|
||||
const localKvps = []
|
||||
for (const key of keys) {
|
||||
// if (!obj[key]) continue // TODO: deal with null/undef
|
||||
const value = obj[key] || obj[key] === 0 ? obj[key] : 'null'
|
||||
const value = !isNullOrUndefined(obj[key]) ? obj[key] : 'null/undefined'
|
||||
localKvps.push({
|
||||
key,
|
||||
value,
|
||||
|
|
|
@ -115,6 +115,11 @@ const {
|
|||
} = useFilterUtilities()
|
||||
|
||||
const revitPropertyRegex = /^parameters\./
|
||||
// Note: we've split this regex check in two to not clash with navis properties. This makes generally makes dim very sad, as we're layering hacks.
|
||||
// Navis object properties come under `properties`, same as revit ones - as such we can't assume they're the same. Here we're targeting revit's
|
||||
// specific two subcategories of `properties`.
|
||||
const revitPropertyRegexDui3000InstanceProps = /^properties\.Instance/ // note this is partially valid for civil3d, or dim should test against it
|
||||
const revitPropertyRegexDui3000TypeProps = /^properties\.Type/ // note this is partially valid for civil3d, or dim should test against it
|
||||
|
||||
const showAllFilters = ref(false)
|
||||
|
||||
|
@ -123,7 +128,11 @@ const props = defineProps<{
|
|||
}>()
|
||||
|
||||
const isRevitProperty = (key: string): boolean => {
|
||||
return revitPropertyRegex.test(key)
|
||||
return (
|
||||
revitPropertyRegex.test(key) ||
|
||||
revitPropertyRegexDui3000InstanceProps.test(key) ||
|
||||
revitPropertyRegexDui3000TypeProps.test(key)
|
||||
)
|
||||
}
|
||||
|
||||
const relevantFilters = computed(() => {
|
||||
|
@ -144,6 +153,9 @@ const relevantFilters = computed(() => {
|
|||
f.key.includes('midPoint.') ||
|
||||
f.key.includes('startPoint.') ||
|
||||
f.key.includes('startPoint.') ||
|
||||
f.key.includes('.materialName') ||
|
||||
f.key.includes('.materialClass') ||
|
||||
f.key.includes('.materialCategory') ||
|
||||
f.key.includes('displayStyle') ||
|
||||
f.key.includes('displayValue') ||
|
||||
f.key.includes('displayMesh')
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
:item="vg"
|
||||
/>
|
||||
<div v-if="itemCount < filter.valueGroups.length" class="mb-2">
|
||||
<FormButton size="sm" text full-width @click="itemCount += 10">
|
||||
<FormButton size="sm" text full-width @click="itemCount += 30">
|
||||
View more ({{ filter.valueGroups.length - itemCount }})
|
||||
</FormButton>
|
||||
</div>
|
||||
|
|
|
@ -76,7 +76,14 @@ const timeOutWait = ref(false)
|
|||
|
||||
const enqueMagic = async () => {
|
||||
isLoading.value = true
|
||||
const screenshot = await viewerInstance.getExtension(PassReader).read()
|
||||
const [depthData, width, height] = await viewerInstance
|
||||
.getExtension(PassReader)
|
||||
.read('DEPTH')
|
||||
const screenshot = PassReader.toBase64(
|
||||
PassReader.decodeDepth(depthData),
|
||||
width,
|
||||
height
|
||||
)
|
||||
void lodgeRequest(screenshot)
|
||||
|
||||
timeOutWait.value = true
|
||||
|
@ -117,7 +124,7 @@ const lodgeRequest = async (screenshot: string) => {
|
|||
const err = getFirstErrorMessage(res.errors)
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to enque Gendo render',
|
||||
title: 'Failed to enqueue Gendo render',
|
||||
description: err
|
||||
})
|
||||
} else {
|
||||
|
|
|
@ -31,7 +31,8 @@
|
|||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="unfold" class="ml-1 space-y-1 px-2 py-1">
|
||||
<div v-if="unfold" class="space-y-1 px-0 py-1">
|
||||
<!-- key value pair display -->
|
||||
<div
|
||||
v-for="(kvp, index) in [
|
||||
...categorisedValuePairs.primitives,
|
||||
|
@ -63,6 +64,9 @@
|
|||
>
|
||||
{{ kvp.value === null ? 'null' : kvp.value }}
|
||||
</span>
|
||||
<span v-if="kvp.units" class="truncate opacity-70">
|
||||
{{ kvp.units }}
|
||||
</span>
|
||||
<button
|
||||
v-if="isCopyable(kvp)"
|
||||
:class="isCopyable(kvp) ? 'cursor-pointer' : 'cursor-default'"
|
||||
|
@ -83,7 +87,7 @@
|
|||
<ViewerSelectionObject
|
||||
:object="(kvp.value as SpeckleObject) || {}"
|
||||
:title="(kvp.key as string)"
|
||||
:unfold="false"
|
||||
:unfold="autoUnfoldKeys.includes(kvp.key)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -156,6 +160,7 @@ const props = withDefaults(
|
|||
const { highlightObjects, unhighlightObjects } = useHighlightedObjectsUtilities()
|
||||
|
||||
const unfold = ref(props.unfold)
|
||||
const autoUnfoldKeys = ['properties', 'Instance Parameters']
|
||||
|
||||
const isAdded = computed(() => {
|
||||
if (!diffEnabled.value) return false
|
||||
|
@ -250,7 +255,7 @@ const ignoredProps = [
|
|||
]
|
||||
|
||||
const keyValuePairs = computed(() => {
|
||||
const kvps = [] as Record<string, unknown>[]
|
||||
const kvps = [] as (Record<string, unknown> & { key: string; value: unknown })[]
|
||||
|
||||
// handle revit paramters
|
||||
if (props.title === 'parameters') {
|
||||
|
@ -273,6 +278,7 @@ const keyValuePairs = computed(() => {
|
|||
const objectKeys = Object.keys(props.object)
|
||||
for (const key of objectKeys) {
|
||||
if (ignoredProps.includes(key)) continue
|
||||
|
||||
const type = Array.isArray(props.object[key]) ? 'array' : typeof props.object[key]
|
||||
let innerType = null
|
||||
let arrayLength = null
|
||||
|
@ -286,6 +292,21 @@ const keyValuePairs = computed(() => {
|
|||
if (arr.length > 10) arrayPreview += ' ...' // in case truncate doesn't hit
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
props.object[key] &&
|
||||
isNameValuePair(props.object[key] as Record<string, unknown>)
|
||||
) {
|
||||
// note: handles name value pairs from dui3 -
|
||||
const { value, units } = props.object[key] as { value: string; units?: string }
|
||||
kvps.push({
|
||||
key,
|
||||
type: typeof value,
|
||||
value: value as string,
|
||||
units
|
||||
})
|
||||
continue
|
||||
}
|
||||
kvps.push({
|
||||
key,
|
||||
type,
|
||||
|
@ -299,14 +320,24 @@ const keyValuePairs = computed(() => {
|
|||
return kvps
|
||||
})
|
||||
|
||||
const isNameValuePair = (obj: Record<string, unknown>) => {
|
||||
const keys = Object.keys(obj)
|
||||
return keys.includes('name') && keys.includes('value')
|
||||
}
|
||||
|
||||
const categorisedValuePairs = computed(() => {
|
||||
return {
|
||||
primitives: keyValuePairs.value.filter(
|
||||
(item) => item.type !== 'object' && item.type !== 'array' && item.value !== null
|
||||
),
|
||||
objects: keyValuePairs.value.filter(
|
||||
(item) => item.type === 'object' && item.value !== null
|
||||
),
|
||||
objects: keyValuePairs.value
|
||||
.filter((item) => item.type === 'object' && item.value !== null)
|
||||
.filter((item) => {
|
||||
const keys = Object.keys(item.value as unknown as Record<string, unknown>)
|
||||
const nvp = keys.includes('name') && keys.includes('value')
|
||||
return !nvp
|
||||
}) // filters out name value pairs - note on new properties structure coming out of DUI3
|
||||
.sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())),
|
||||
nonPrimitiveArrays: keyValuePairs.value.filter(
|
||||
(item) =>
|
||||
item.type === 'array' &&
|
||||
|
|
|
@ -23,12 +23,11 @@
|
|||
label="Short ID"
|
||||
:help="getShortIdHelp"
|
||||
color="foundation"
|
||||
:rules="[
|
||||
isStringOfLength({ maxLength: 50, minLength: 3 }),
|
||||
isValidWorkspaceSlug
|
||||
]"
|
||||
:loading="loading"
|
||||
:rules="isStringOfLength({ maxLength: 50, minLength: 3 })"
|
||||
:custom-error-message="error?.graphQLErrors[0]?.message"
|
||||
show-label
|
||||
@update:model-value="shortIdManuallyEdited = true"
|
||||
@update:model-value="onSlugChange"
|
||||
/>
|
||||
<UserAvatarEditable
|
||||
v-model:edit-mode="editAvatarMode"
|
||||
|
@ -49,13 +48,11 @@ import type { MaybeNullOrUndefined } from '@speckle/shared'
|
|||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { useCreateWorkspace } from '~/lib/workspaces/composables/management'
|
||||
import { useWorkspacesAvatar } from '~/lib/workspaces/composables/avatar'
|
||||
import {
|
||||
isRequired,
|
||||
isStringOfLength,
|
||||
isValidWorkspaceSlug
|
||||
} from '~~/lib/common/helpers/validation'
|
||||
import { isRequired, isStringOfLength } from '~~/lib/common/helpers/validation'
|
||||
import { generateSlugFromName } from '@speckle/shared'
|
||||
import { debounce } from 'lodash'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { validateWorkspaceSlugQuery } from '~/lib/workspaces/graphql/queries'
|
||||
|
||||
const emit = defineEmits<(e: 'created') => void>()
|
||||
|
||||
|
@ -69,15 +66,25 @@ const isOpen = defineModel<boolean>('open', { required: true })
|
|||
|
||||
const createWorkspace = useCreateWorkspace()
|
||||
const { generateDefaultLogoIndex, getDefaultAvatar } = useWorkspacesAvatar()
|
||||
const { handleSubmit } = useForm<{ name: string; slug: string }>()
|
||||
const { handleSubmit, resetForm } = useForm<{ name: string; slug: string }>()
|
||||
|
||||
const workspaceName = ref('')
|
||||
const workspaceShortId = ref('')
|
||||
const debouncedWorkspaceShortId = ref('')
|
||||
const editAvatarMode = ref(false)
|
||||
const workspaceLogo = ref<MaybeNullOrUndefined<string>>()
|
||||
const defaultLogoIndex = ref(0)
|
||||
const shortIdManuallyEdited = ref(false)
|
||||
const customShortIdError = ref('')
|
||||
|
||||
const { error, loading } = useQuery(
|
||||
validateWorkspaceSlugQuery,
|
||||
() => ({
|
||||
slug: debouncedWorkspaceShortId.value
|
||||
}),
|
||||
() => ({
|
||||
enabled: !!debouncedWorkspaceShortId.value
|
||||
})
|
||||
)
|
||||
|
||||
const baseUrl = useRuntimeConfig().public.baseUrl
|
||||
|
||||
|
@ -105,7 +112,7 @@ const dialogButtons = computed((): LayoutDialogButton[] => [
|
|||
disabled:
|
||||
!workspaceName.value.trim() ||
|
||||
!workspaceShortId.value.trim() ||
|
||||
!!customShortIdError.value
|
||||
error.value !== null
|
||||
}
|
||||
}
|
||||
])
|
||||
|
@ -122,7 +129,7 @@ const handleCreateWorkspace = handleSubmit(async () => {
|
|||
{ source: props.eventSource }
|
||||
)
|
||||
|
||||
if (newWorkspace) {
|
||||
if (newWorkspace && !newWorkspace?.errors) {
|
||||
emit('created')
|
||||
isOpen.value = false
|
||||
}
|
||||
|
@ -135,21 +142,36 @@ const onLogoSave = (newVal: MaybeNullOrUndefined<string>) => {
|
|||
|
||||
const reset = () => {
|
||||
defaultLogoIndex.value = generateDefaultLogoIndex()
|
||||
workspaceName.value = ''
|
||||
workspaceShortId.value = ''
|
||||
debouncedWorkspaceShortId.value = ''
|
||||
workspaceLogo.value = null
|
||||
editAvatarMode.value = false
|
||||
shortIdManuallyEdited.value = false
|
||||
customShortIdError.value = ''
|
||||
error.value = null
|
||||
}
|
||||
|
||||
const updateShortId = debounce((newName: string) => {
|
||||
if (!shortIdManuallyEdited.value) {
|
||||
workspaceShortId.value = generateSlugFromName({ name: newName })
|
||||
const newSlug = generateSlugFromName({ name: newName })
|
||||
workspaceShortId.value = newSlug
|
||||
updateDebouncedShortId(newSlug)
|
||||
}
|
||||
}, 600)
|
||||
|
||||
const updateDebouncedShortId = debounce((newSlug: string) => {
|
||||
debouncedWorkspaceShortId.value = newSlug
|
||||
}, 300)
|
||||
|
||||
const onSlugChange = (newSlug: string) => {
|
||||
workspaceShortId.value = newSlug
|
||||
shortIdManuallyEdited.value = true
|
||||
updateDebouncedShortId(newSlug)
|
||||
}
|
||||
|
||||
// Seperate resets to avoid a temporary invalid state on submission
|
||||
watch(isOpen, (newVal) => {
|
||||
if (newVal) reset()
|
||||
if (!newVal) {
|
||||
reset()
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<span
|
||||
v-tippy="
|
||||
project.role !== Roles.Stream.Owner &&
|
||||
'Only project owners can move projects'
|
||||
'Only the project owner can move this project'
|
||||
"
|
||||
>
|
||||
<FormButton
|
||||
|
@ -44,7 +44,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<p v-else class="py-4 text-body-xs text-foreground-2">
|
||||
You don't have any projects that are moveable to this workspace
|
||||
You don't have any projects that can be moved into this workspace
|
||||
</p>
|
||||
|
||||
<ProjectsMoveToWorkspaceDialog
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
<FormButton
|
||||
v-else
|
||||
class="hidden md:block"
|
||||
color="outline"
|
||||
color="subtle"
|
||||
@click="showMoveProjectsDialog = true"
|
||||
>
|
||||
Move projects
|
||||
|
@ -277,6 +277,9 @@ const onShowSettingsDialog = (target: AvailableSettingsMenuKeys) => {
|
|||
onResult((queryResult) => {
|
||||
if (queryResult.data?.workspaceBySlug) {
|
||||
workspaceMixpanelUpdateGroup(queryResult.data.workspaceBySlug)
|
||||
useHeadSafe({
|
||||
title: queryResult.data.workspaceBySlug.name
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -35,14 +35,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between md:items-center gap-x-3 md:flex-row"
|
||||
:class="[isWorkspaceAdmin ? 'flex-col' : 'flex-row items-center']"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-x-3 md:mb-0"
|
||||
:class="[isWorkspaceAdmin ? 'mb-3' : ' flex-1']"
|
||||
>
|
||||
<div class="flex justify-between items-center gap-x-3">
|
||||
<div class="flex items-center gap-x-3 md:mb-0 flex-1">
|
||||
<CommonBadge rounded :color-classes="'text-foreground-2 bg-primary-muted'">
|
||||
{{ workspaceInfo.totalProjects.totalCount || 0 }} Project{{
|
||||
workspaceInfo.totalProjects.totalCount === 1 ? '' : 's'
|
||||
|
@ -55,26 +49,22 @@
|
|||
</CommonBadge>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div v-if="workspaceInfo.billing" class="flex-1 md:flex-auto">
|
||||
<button
|
||||
class="block"
|
||||
@click="openSettingsDialog(SettingMenuKeys.Workspace.Billing)"
|
||||
>
|
||||
<WorkspacePageVersionCount
|
||||
:versions-count="workspaceInfo.billing.versionsCount"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-3">
|
||||
<button
|
||||
class="block"
|
||||
@click="openSettingsDialog(SettingMenuKeys.Workspace.Members)"
|
||||
<div
|
||||
v-if="!isWorkspaceGuest"
|
||||
v-tippy="isWorkspaceAdmin ? 'Manage members' : 'View members'"
|
||||
>
|
||||
<UserAvatarGroup
|
||||
:users="team.map((teamMember) => teamMember.user)"
|
||||
class="max-w-[104px]"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="block"
|
||||
@click="openSettingsDialog(SettingMenuKeys.Workspace.Members)"
|
||||
>
|
||||
<UserAvatarGroup
|
||||
:users="team.map((teamMember) => teamMember.user)"
|
||||
class="max-w-[104px]"
|
||||
hide-tooltips
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<FormButton
|
||||
v-if="isWorkspaceAdmin"
|
||||
class="hidden md:block"
|
||||
|
@ -83,11 +73,20 @@
|
|||
>
|
||||
Invite
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-if="isWorkspaceAdmin"
|
||||
class="hidden md:block"
|
||||
color="outline"
|
||||
@click="openSettingsDialog(SettingMenuKeys.Workspace.General)"
|
||||
>
|
||||
Settings
|
||||
</FormButton>
|
||||
<LayoutMenu
|
||||
v-model:open="showActionsMenu"
|
||||
:items="actionsItems"
|
||||
:menu-position="HorizontalDirection.Left"
|
||||
:menu-id="menuId"
|
||||
class="md:hidden"
|
||||
@click.stop.prevent
|
||||
@chosen="onActionChosen"
|
||||
>
|
||||
|
@ -136,11 +135,6 @@ graphql(`
|
|||
totalProjects: projects {
|
||||
totalCount
|
||||
}
|
||||
billing {
|
||||
versionsCount {
|
||||
...WorkspacePageVersionCount_WorkspaceVersionsCount
|
||||
}
|
||||
}
|
||||
team {
|
||||
items {
|
||||
id
|
||||
|
@ -189,17 +183,17 @@ const actionsItems = computed<LayoutMenuItem[][]>(() => [
|
|||
[
|
||||
...(isMobile.value
|
||||
? [
|
||||
...(isWorkspaceAdmin.value
|
||||
? [{ title: 'Move projects', id: ActionTypes.MoveProjects }]
|
||||
: []),
|
||||
{ title: 'Settings', id: ActionTypes.Settings },
|
||||
|
||||
...(!isWorkspaceGuest.value
|
||||
? [{ title: 'Invite', id: ActionTypes.Invite }]
|
||||
? [{ title: 'Invite...', id: ActionTypes.Invite }]
|
||||
: []),
|
||||
...(isWorkspaceAdmin.value
|
||||
? [{ title: 'Move projects...', id: ActionTypes.MoveProjects }]
|
||||
: [])
|
||||
]
|
||||
: []),
|
||||
{ title: 'Copy link', id: ActionTypes.CopyLink }
|
||||
],
|
||||
[{ title: 'Settings...', id: ActionTypes.Settings }]
|
||||
: [])
|
||||
]
|
||||
])
|
||||
|
||||
const openSettingsDialog = (target: AvailableSettingsMenuKeys) => {
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
<template>
|
||||
<div class="flex flex-col">
|
||||
<WorkspaceInviteBanner
|
||||
v-for="invite in invites"
|
||||
:key="invite.id"
|
||||
:invite="invite"
|
||||
/>
|
||||
<WorkspaceInviteDiscoverableWorkspaceBanner
|
||||
v-for="workspace in discoverableWorkspaces"
|
||||
:key="workspace.id"
|
||||
:workspace="workspace"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useSynchronizedCookie } from '~/lib/common/composables/reactiveCookie'
|
||||
import { CookieKeys } from '~/lib/common/helpers/constants'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type { WorkspaceInviteBanners_UserFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
|
||||
/**
|
||||
* TODO: Add this to new dashboard page and remove from projects dashboard
|
||||
*/
|
||||
|
||||
graphql(`
|
||||
fragment WorkspaceInviteBanners_User on User {
|
||||
discoverableWorkspaces {
|
||||
...WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace
|
||||
}
|
||||
workspaceInvites {
|
||||
...WorkspaceInviteBanner_PendingWorkspaceCollaborator
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
invites: WorkspaceInviteBanners_UserFragment
|
||||
}>()
|
||||
|
||||
const dismissedDiscoverableWorkspaces = useSynchronizedCookie<string[]>(
|
||||
CookieKeys.DismissedDiscoverableWorkspaces,
|
||||
{
|
||||
default: () => []
|
||||
}
|
||||
)
|
||||
|
||||
const invites = computed(() => props.invites.workspaceInvites || [])
|
||||
const discoverableWorkspaces = computed(
|
||||
() =>
|
||||
props.invites.discoverableWorkspaces?.filter(
|
||||
(workspace) => !dismissedDiscoverableWorkspaces.value.includes(workspace.id)
|
||||
) || []
|
||||
)
|
||||
</script>
|
|
@ -14,7 +14,7 @@ import { useSynchronizedCookie } from '~/lib/common/composables/reactiveCookie'
|
|||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import {
|
||||
DashboardJoinWorkspaceDocument,
|
||||
type WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspaceFragment
|
||||
type WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { CookieKeys } from '~/lib/common/helpers/constants'
|
||||
import {
|
||||
|
@ -22,12 +22,14 @@ import {
|
|||
getFirstErrorMessage,
|
||||
modifyObjectField
|
||||
} from '~/lib/common/helpers/graphql'
|
||||
import { workspaceRoute } from '~/lib/common/helpers/route'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
|
||||
graphql(`
|
||||
fragment WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace on DiscoverableWorkspace {
|
||||
fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {
|
||||
id
|
||||
name
|
||||
slug
|
||||
description
|
||||
logo
|
||||
defaultLogoIndex
|
||||
|
@ -46,7 +48,7 @@ graphql(`
|
|||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
workspace: WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspaceFragment
|
||||
workspace: WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragment
|
||||
}>()
|
||||
|
||||
const mixpanel = useMixpanel()
|
||||
|
@ -76,7 +78,7 @@ const processJoin = async (accept: boolean) => {
|
|||
props.workspace.id
|
||||
]
|
||||
apollo.cache.evict({
|
||||
id: getCacheId('DiscoverableWorkspace', props.workspace.id)
|
||||
id: getCacheId('LimitedWorkspace', props.workspace.id)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -115,7 +117,7 @@ const processJoin = async (accept: boolean) => {
|
|||
|
||||
if (result?.data) {
|
||||
apollo.cache.evict({
|
||||
id: getCacheId('DiscoverableWorkspace', props.workspace.id)
|
||||
id: getCacheId('LimitedWorkspace', props.workspace.id)
|
||||
})
|
||||
|
||||
triggerNotification({
|
||||
|
@ -130,7 +132,7 @@ const processJoin = async (accept: boolean) => {
|
|||
workspace_id: props.workspace.id
|
||||
})
|
||||
|
||||
router.push(`/workspaces/${props.workspace.id}`)
|
||||
router.push(workspaceRoute(props.workspace.slug))
|
||||
} else {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
<template>
|
||||
<div class="w-40 flex flex-col items-center md:mx-4">
|
||||
<CommonProgressBar
|
||||
class="mb-1"
|
||||
:current-value="versionsCount.current"
|
||||
:max-value="versionsCount.max"
|
||||
/>
|
||||
<div class="text-body-3xs text-foreground">
|
||||
<span class="font-medium">
|
||||
{{ versionsCount.current }}/{{ versionsCount.max }}
|
||||
</span>
|
||||
model versions used
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { WorkspacePageVersionCount_WorkspaceVersionsCountFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment WorkspacePageVersionCount_WorkspaceVersionsCount on WorkspaceVersionsCount {
|
||||
current
|
||||
max
|
||||
}
|
||||
`)
|
||||
|
||||
defineProps<{
|
||||
versionsCount: WorkspacePageVersionCount_WorkspaceVersionsCountFragment
|
||||
}>()
|
||||
</script>
|
|
@ -38,9 +38,9 @@
|
|||
</CommonCard>
|
||||
|
||||
<CommonCard
|
||||
title="Sovereign Data Regions"
|
||||
title="Data residency"
|
||||
badge="Coming soon"
|
||||
description="Store each project's data in the geographical location that you need, with granular precision going beyond continents."
|
||||
description="Store your workspace projects in the geographical region of your choice."
|
||||
>
|
||||
<template #icon>
|
||||
<GlobeAltIcon class="size-6 text-foreground-2 ml-1" />
|
||||
|
@ -49,7 +49,7 @@
|
|||
|
||||
<CommonCard
|
||||
title="... and more!"
|
||||
description="We will be rolling out new features, like advanced permissions, audit logs, bigger uploads and more over the coming months."
|
||||
description="We will be rolling out new features, like advanced permissions, audit logs, and more over the coming months."
|
||||
>
|
||||
<template #icon>
|
||||
<PlusIcon class="size-6 text-foreground-2 ml-1" />
|
||||
|
|
|
@ -41,4 +41,11 @@ export const useIsGendoModuleEnabled = () => {
|
|||
return ref(FF_GENDOAI_MODULE_ENABLED)
|
||||
}
|
||||
|
||||
export const useIsBillingIntegrationEnabled = () => {
|
||||
const {
|
||||
public: { FF_BILLING_INTEGRATION_ENABLED }
|
||||
} = useRuntimeConfig()
|
||||
return ref(FF_BILLING_INTEGRATION_ENABLED)
|
||||
}
|
||||
|
||||
export { useGlobalToast, useActiveUser, usePageQueryStandardFetchPolicy }
|
||||
|
|
|
@ -382,6 +382,24 @@ export const useAuthManager = (
|
|||
goHome({ query: { access_code: accessCode } })
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate SSO flow. Will create a user if one does not already exist.
|
||||
*/
|
||||
const signInOrSignUpWithSso = (params: {
|
||||
challenge: string
|
||||
workspaceSlug: string
|
||||
}) => {
|
||||
postAuthRedirect.set(`/workspaces/${params.workspaceSlug}`)
|
||||
|
||||
const authUrl = new URL(
|
||||
`/api/v1/workspaces/${params.workspaceSlug}/sso/auth`,
|
||||
apiOrigin
|
||||
)
|
||||
authUrl.searchParams.set('challenge', params.challenge)
|
||||
|
||||
navigateTo(authUrl.toString(), { external: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Log out
|
||||
*/
|
||||
|
@ -425,6 +443,7 @@ export const useAuthManager = (
|
|||
authToken,
|
||||
loginWithEmail,
|
||||
signUpWithEmail,
|
||||
signInOrSignUpWithSso,
|
||||
logout,
|
||||
watchAuthQueryString,
|
||||
inviteToken
|
||||
|
|
|
@ -11,7 +11,7 @@ export function useServerFileUploadLimit() {
|
|||
const { result } = useQuery(serverInfoBlobSizeLimitQuery)
|
||||
|
||||
const maxSizeInBytes = computed(
|
||||
() => result.value?.serverInfo.blobSizeLimitBytes || 0
|
||||
() => result.value?.serverInfo.configuration.blobSizeLimitBytes || 0
|
||||
)
|
||||
const maxSizeDisplayString = computed(() => prettyFileSize(maxSizeInBytes.value))
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ const documents = {
|
|||
"\n subscription OnUserProjectsUpdate {\n userProjectsUpdated {\n type\n id\n project {\n ...ProjectDashboardItem\n }\n }\n }\n ": types.OnUserProjectsUpdateDocument,
|
||||
"\n fragment ProjectsDashboardFilled on ProjectCollection {\n items {\n ...ProjectDashboardItem\n }\n }\n": types.ProjectsDashboardFilledFragmentDoc,
|
||||
"\n fragment ProjectsDashboardHeaderProjects_User on User {\n ...ProjectsInviteBanners\n }\n": types.ProjectsDashboardHeaderProjects_UserFragmentDoc,
|
||||
"\n fragment ProjectsDashboardHeaderWorkspaces_User on User {\n ...WorkspaceInviteBanners_User\n }\n": types.ProjectsDashboardHeaderWorkspaces_UserFragmentDoc,
|
||||
"\n fragment ProjectsDashboardHeaderWorkspaces_User on User {\n discoverableWorkspaces {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace\n }\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n": types.ProjectsDashboardHeaderWorkspaces_UserFragmentDoc,
|
||||
"\n fragment ProjectsMoveToWorkspaceDialog_Workspace on Workspace {\n id\n role\n name\n defaultLogoIndex\n logo\n }\n": types.ProjectsMoveToWorkspaceDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment ProjectsMoveToWorkspaceDialog_User on User {\n workspaces {\n items {\n ...ProjectsMoveToWorkspaceDialog_Workspace\n }\n }\n }\n": types.ProjectsMoveToWorkspaceDialog_UserFragmentDoc,
|
||||
"\n fragment ProjectsMoveToWorkspaceDialog_Project on Project {\n id\n name\n modelCount: models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n }\n": types.ProjectsMoveToWorkspaceDialog_ProjectFragmentDoc,
|
||||
|
@ -101,6 +101,9 @@ const documents = {
|
|||
"\n fragment SettingsDialog_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n slug\n role\n name\n }\n": types.SettingsDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsDialog_User on User {\n id\n workspaces {\n items {\n ...SettingsDialog_Workspace\n }\n }\n }\n": types.SettingsDialog_UserFragmentDoc,
|
||||
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsServerProjects_ProjectCollectionFragmentDoc,
|
||||
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": types.SettingsServerRegionsDocument,
|
||||
"\n fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {\n id\n name\n description\n key\n }\n": types.SettingsServerRegionsAddEditDialog_ServerRegionItemFragmentDoc,
|
||||
"\n fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {\n id\n name\n key\n description\n }\n": types.SettingsServerRegionsTable_ServerRegionItemFragmentDoc,
|
||||
"\n fragment SettingsSharedProjects_Project on Project {\n id\n name\n visibility\n createdAt\n updatedAt\n models {\n totalCount\n }\n versions {\n totalCount\n }\n team {\n id\n user {\n name\n id\n avatar\n }\n }\n }\n": types.SettingsSharedProjects_ProjectFragmentDoc,
|
||||
"\n fragment SettingsUserEmails_User on User {\n id\n emails {\n ...SettingsUserEmailCards_UserEmail\n }\n }\n": types.SettingsUserEmails_UserFragmentDoc,
|
||||
"\n fragment SettingsUserNotifications_User on User {\n id\n notificationPreferences\n }\n": types.SettingsUserNotifications_UserFragmentDoc,
|
||||
|
@ -110,13 +113,14 @@ const documents = {
|
|||
"\n fragment SettingsUserProfileDeleteAccount_User on User {\n id\n email\n }\n": types.SettingsUserProfileDeleteAccount_UserFragmentDoc,
|
||||
"\n fragment SettingsUserProfileDetails_User on User {\n id\n name\n company\n ...UserProfileEditDialogAvatar_User\n }\n": types.SettingsUserProfileDetails_UserFragmentDoc,
|
||||
"\n fragment UserProfileEditDialogAvatar_User on User {\n id\n avatar\n ...ActiveUserAvatar\n }\n": types.UserProfileEditDialogAvatar_UserFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n billing {\n cost {\n subTotal\n total\n ...BillingSummary_WorkspaceCost\n }\n versionsCount {\n current\n max\n }\n }\n }\n": types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n id\n plan {\n name\n status\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n": types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n defaultProjectRole\n }\n": types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspaceGeneralDeleteDialog_Workspace on Workspace {\n id\n name\n }\n": types.SettingsWorkspaceGeneralDeleteDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesGeneralEditAvatar_Workspace on Workspace {\n id\n logo\n name\n defaultLogoIndex\n }\n": types.SettingsWorkspacesGeneralEditAvatar_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesGeneralEditSlugDialog_Workspace on Workspace {\n id\n name\n slug\n }\n": types.SettingsWorkspacesGeneralEditSlugDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n role\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n }\n": types.SettingsWorkspacesMembers_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsWorkspacesProjects_ProjectCollectionFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n availableRegions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n": types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n ...SettingsWorkspacesSecuritySso_Workspace\n }\n\n fragment SettingsWorkspacesSecurity_User on User {\n id\n emails {\n id\n email\n verified\n }\n }\n": types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n user {\n id\n avatar\n name\n company\n verified\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team {\n items {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceFragmentDoc,
|
||||
|
@ -125,6 +129,7 @@ const documents = {
|
|||
"\n fragment SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n user {\n id\n avatar\n name\n company\n verified\n workspaceDomainPolicyCompliant(workspaceId: $workspaceId)\n }\n }\n": types.SettingsWorkspacesMembersMembersTable_WorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersMembersTable_Workspace on Workspace {\n id\n name\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team {\n items {\n id\n ...SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersMembersTable_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...WorkspaceInviteDialog_Workspace\n }\n": types.SettingsWorkspacesMembersTableHeader_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegionsSelect_ServerRegionItem on ServerRegionItem {\n id\n key\n name\n description\n }\n": types.SettingsWorkspacesRegionsSelect_ServerRegionItemFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain on WorkspaceDomain {\n id\n domain\n }\n": types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n": types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecuritySso_Workspace on Workspace {\n id\n slug\n }\n": types.SettingsWorkspacesSecuritySso_WorkspaceFragmentDoc,
|
||||
|
@ -133,16 +138,14 @@ const documents = {
|
|||
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": types.ViewerCommentsListItemFragmentDoc,
|
||||
"\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": types.ViewerModelVersionCardItemFragmentDoc,
|
||||
"\n fragment WorkspaceAvatar_Workspace on Workspace {\n id\n logo\n defaultLogoIndex\n }\n": types.WorkspaceAvatar_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n id\n team {\n items {\n id\n user {\n id\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n": types.WorkspaceInviteDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n id\n team {\n items {\n id\n user {\n id\n role\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n": types.WorkspaceInviteDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment MoveProjectsDialog_Workspace on Workspace {\n id\n ...ProjectsMoveToWorkspaceDialog_Workspace\n projects {\n items {\n id\n modelCount: models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n }\n }\n }\n": types.MoveProjectsDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment MoveProjectsDialog_User on User {\n projects {\n items {\n ...ProjectsMoveToWorkspaceDialog_Project\n role\n workspace {\n id\n }\n }\n }\n }\n": types.MoveProjectsDialog_UserFragmentDoc,
|
||||
"\n fragment WorkspaceProjectList_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...ProjectDashboardItem\n }\n cursor\n }\n": types.WorkspaceProjectList_ProjectCollectionFragmentDoc,
|
||||
"\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n slug\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n billing {\n versionsCount {\n ...WorkspacePageVersionCount_WorkspaceVersionsCount\n }\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n": types.WorkspaceHeader_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n slug\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n": types.WorkspaceHeader_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceInviteBanner_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.WorkspaceInviteBanner_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment WorkspaceInviteBanners_User on User {\n discoverableWorkspaces {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace\n }\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n": types.WorkspaceInviteBanners_UserFragmentDoc,
|
||||
"\n fragment WorkspaceInviteBlock_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n workspaceId\n workspaceName\n token\n user {\n id\n name\n ...LimitedUserAvatar\n }\n title\n email\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.WorkspaceInviteBlock_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace on DiscoverableWorkspace {\n id\n name\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n": types.WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspaceFragmentDoc,
|
||||
"\n fragment WorkspacePageVersionCount_WorkspaceVersionsCount on WorkspaceVersionsCount {\n current\n max\n }\n": types.WorkspacePageVersionCount_WorkspaceVersionsCountFragmentDoc,
|
||||
"\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n": types.WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragmentDoc,
|
||||
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n": types.ActiveUserMainMetadataDocument,
|
||||
"\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n ": types.CreateOnboardingProjectDocument,
|
||||
"\n mutation FinishOnboarding {\n activeUserMutations {\n finishOnboarding\n }\n }\n": types.FinishOnboardingDocument,
|
||||
|
@ -163,10 +166,10 @@ const documents = {
|
|||
"\n query AutomateFunctionsPagePagination($search: String, $cursor: String) {\n ...AutomateFunctionsPageItems_Query\n }\n": types.AutomateFunctionsPagePaginationDocument,
|
||||
"\n query MentionsUserSearch($query: String!, $emailOnly: Boolean = false) {\n userSearch(\n query: $query\n limit: 5\n cursor: null\n archived: false\n emailOnly: $emailOnly\n ) {\n items {\n id\n name\n company\n }\n }\n }\n": types.MentionsUserSearchDocument,
|
||||
"\n query UserSearch(\n $query: String!\n $limit: Int\n $cursor: String\n $archived: Boolean\n $workspaceId: String\n ) {\n userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {\n cursor\n items {\n id\n name\n bio\n company\n avatar\n verified\n role\n workspaceDomainPolicyCompliant(workspaceId: $workspaceId)\n }\n }\n }\n": types.UserSearchDocument,
|
||||
"\n query ServerInfoBlobSizeLimit {\n serverInfo {\n blobSizeLimitBytes\n }\n }\n": types.ServerInfoBlobSizeLimitDocument,
|
||||
"\n query ServerInfoBlobSizeLimit {\n serverInfo {\n configuration {\n blobSizeLimitBytes\n }\n }\n }\n": types.ServerInfoBlobSizeLimitDocument,
|
||||
"\n query ServerInfoAllScopes {\n serverInfo {\n scopes {\n name\n description\n }\n }\n }\n": types.ServerInfoAllScopesDocument,
|
||||
"\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectModelsSelectorValuesDocument,
|
||||
"\n query MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n": types.MainServerInfoDataDocument,
|
||||
"\n query MainServerInfoData {\n serverInfo {\n adminContact\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n": types.MainServerInfoDataDocument,
|
||||
"\n mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {\n workspaceMutations {\n join(input: $input) {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace\n }\n }\n }\n": types.DashboardJoinWorkspaceDocument,
|
||||
"\n query DashboardProjectsPageQuery {\n activeUser {\n id\n projects(limit: 3) {\n items {\n ...DashboardProjectCard_Project\n }\n }\n ...ProjectsDashboardHeaderProjects_User\n }\n }\n": types.DashboardProjectsPageQueryDocument,
|
||||
"\n query DashboardProjectsPageWorkspaceQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n": types.DashboardProjectsPageWorkspaceQueryDocument,
|
||||
|
@ -186,6 +189,8 @@ const documents = {
|
|||
"\n query GendoAIRenders($versionId: String!, $projectId: String!) {\n project(id: $projectId) {\n id\n version(id: $versionId) {\n id\n gendoAIRenders {\n totalCount\n items {\n id\n createdAt\n updatedAt\n status\n gendoGenerationId\n prompt\n camera\n }\n }\n }\n }\n }\n": types.GendoAiRendersDocument,
|
||||
"\n subscription ProjectVersionGendoAIRenderCreated($id: String!, $versionId: String!) {\n projectVersionGendoAIRenderCreated(id: $id, versionId: $versionId) {\n id\n createdAt\n updatedAt\n status\n gendoGenerationId\n prompt\n camera\n }\n }\n": types.ProjectVersionGendoAiRenderCreatedDocument,
|
||||
"\n subscription ProjectVersionGendoAIRenderUpdated($id: String!, $versionId: String!) {\n projectVersionGendoAIRenderUpdated(id: $id, versionId: $versionId) {\n id\n projectId\n modelId\n versionId\n createdAt\n updatedAt\n gendoGenerationId\n status\n prompt\n camera\n responseImage\n }\n }\n": types.ProjectVersionGendoAiRenderUpdatedDocument,
|
||||
"\n mutation CreateNewRegion($input: CreateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n create(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n": types.CreateNewRegionDocument,
|
||||
"\n mutation UpdateRegion($input: UpdateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n update(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n": types.UpdateRegionDocument,
|
||||
"\n fragment ProjectPageTeamInternals_Project on Project {\n id\n role\n invitedTeam {\n id\n title\n role\n inviteId\n user {\n role\n ...LimitedUserAvatar\n }\n }\n team {\n role\n user {\n id\n role\n ...LimitedUserAvatar\n }\n }\n }\n": types.ProjectPageTeamInternals_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageTeamInternals_Workspace on Workspace {\n id\n team {\n items {\n id\n role\n user {\n id\n }\n }\n }\n }\n": types.ProjectPageTeamInternals_WorkspaceFragmentDoc,
|
||||
"\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n id\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n": types.ProjectDashboardItemNoModelsFragmentDoc,
|
||||
|
@ -198,6 +203,7 @@ const documents = {
|
|||
"\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n }\n": types.ProjectPageLatestItemsCommentItemFragmentDoc,
|
||||
"\n mutation CreateModel($input: CreateModelInput!) {\n modelMutations {\n create(input: $input) {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n": types.CreateModelDocument,
|
||||
"\n mutation CreateProject($input: ProjectCreateInput) {\n projectMutations {\n create(input: $input) {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n": types.CreateProjectDocument,
|
||||
"\n mutation CreateWorkspaceProject($input: WorkspaceProjectCreateInput!) {\n workspaceMutations {\n projects {\n create(input: $input) {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n }\n": types.CreateWorkspaceProjectDocument,
|
||||
"\n mutation UpdateModel($input: UpdateModelInput!) {\n modelMutations {\n update(input: $input) {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n": types.UpdateModelDocument,
|
||||
"\n mutation DeleteModel($input: DeleteModelInput!) {\n modelMutations {\n delete(input: $input)\n }\n }\n": types.DeleteModelDocument,
|
||||
"\n mutation UpdateProjectRole($input: ProjectUpdateRoleInput!) {\n projectMutations {\n updateRole(input: $input) {\n id\n team {\n id\n role\n user {\n ...LimitedUserAvatar\n }\n }\n }\n }\n }\n": types.UpdateProjectRoleDocument,
|
||||
|
@ -284,6 +290,9 @@ const documents = {
|
|||
"\n query SettingsSidebar {\n activeUser {\n ...SettingsDialog_User\n }\n }\n": types.SettingsSidebarDocument,
|
||||
"\n query SettingsWorkspaceGeneral($id: String!) {\n workspace(id: $id) {\n ...SettingsWorkspacesGeneral_Workspace\n }\n }\n": types.SettingsWorkspaceGeneralDocument,
|
||||
"\n query SettingsWorkspaceBilling($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n": types.SettingsWorkspaceBillingDocument,
|
||||
"\n query SettingsWorkspacePricingPlans {\n workspacePricingPlans\n }\n": types.SettingsWorkspacePricingPlansDocument,
|
||||
"\n query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {\n workspace(id: $workspaceId) {\n customerPortalUrl\n }\n }\n": types.SettingsWorkspaceBillingCustomerPortalDocument,
|
||||
"\n query SettingsWorkspaceRegions($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...SettingsWorkspacesRegions_Workspace\n }\n }\n": types.SettingsWorkspaceRegionsDocument,
|
||||
"\n query SettingsWorkspacesMembers(\n $workspaceId: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspace(id: $workspaceId) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n": types.SettingsWorkspacesMembersDocument,
|
||||
"\n query SettingsWorkspacesMembersSearch(\n $workspaceId: String!\n $filter: WorkspaceTeamFilter\n ) {\n workspace(id: $workspaceId) {\n id\n team(filter: $filter) {\n items {\n id\n ...SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator\n }\n }\n }\n }\n": types.SettingsWorkspacesMembersSearchDocument,
|
||||
"\n query SettingsWorkspacesInvitesSearch(\n $workspaceId: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspace(id: $workspaceId) {\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n": types.SettingsWorkspacesInvitesSearchDocument,
|
||||
|
@ -321,11 +330,13 @@ const documents = {
|
|||
"\n mutation InviteToWorkspace(\n $workspaceId: String!\n $input: [WorkspaceInviteCreateInput!]!\n ) {\n workspaceMutations {\n invites {\n batchCreate(workspaceId: $workspaceId, input: $input) {\n id\n invitedTeam {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n }\n }\n }\n": types.InviteToWorkspaceDocument,
|
||||
"\n mutation CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n ...SettingsDialog_Workspace\n }\n }\n }\n": types.CreateWorkspaceDocument,
|
||||
"\n mutation ProcessWorkspaceInvite($input: WorkspaceInviteUseInput!) {\n workspaceMutations {\n invites {\n use(input: $input)\n }\n }\n }\n": types.ProcessWorkspaceInviteDocument,
|
||||
"\n mutation SetDefaultWorkspaceRegion($workspaceId: String!, $regionKey: String!) {\n workspaceMutations {\n setDefaultRegion(workspaceId: $workspaceId, regionKey: $regionKey) {\n id\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n }\n": types.SetDefaultWorkspaceRegionDocument,
|
||||
"\n query WorkspaceAccessCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n }\n }\n": types.WorkspaceAccessCheckDocument,
|
||||
"\n query WorkspacePageQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n $token: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n ...MoveProjectsDialog_Workspace\n ...WorkspaceHeader_Workspace\n ...WorkspaceMixpanelUpdateGroup_Workspace\n projectListProject: projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceProjectList_ProjectCollection\n }\n }\n workspaceInvite(\n workspaceId: $workspaceSlug\n token: $token\n options: { useSlug: true }\n ) {\n id\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n ...WorkspaceInviteBlock_PendingWorkspaceCollaborator\n }\n }\n": types.WorkspacePageQueryDocument,
|
||||
"\n query WorkspaceProjectsQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceProjectList_ProjectCollection\n }\n }\n }\n": types.WorkspaceProjectsQueryDocument,
|
||||
"\n query WorkspaceInvite(\n $workspaceId: String\n $token: String\n $options: WorkspaceInviteLookupOptions\n ) {\n workspaceInvite(workspaceId: $workspaceId, token: $token, options: $options) {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n ...WorkspaceInviteBlock_PendingWorkspaceCollaborator\n }\n }\n": types.WorkspaceInviteDocument,
|
||||
"\n query MoveProjectsDialog {\n activeUser {\n ...MoveProjectsDialog_User\n }\n }\n": types.MoveProjectsDialogDocument,
|
||||
"\n query ValidateWorkspaceSlug($slug: String!) {\n validateWorkspaceSlug(slug: $slug)\n }\n": types.ValidateWorkspaceSlugDocument,
|
||||
"\n query LegacyBranchRedirectMetadata($streamId: String!, $branchName: String!) {\n project(id: $streamId) {\n modelByName(name: $branchName) {\n id\n }\n }\n }\n": types.LegacyBranchRedirectMetadataDocument,
|
||||
"\n query LegacyViewerCommitRedirectMetadata($streamId: String!, $commitId: String!) {\n project(id: $streamId) {\n version(id: $commitId) {\n id\n model {\n id\n }\n }\n }\n }\n": types.LegacyViewerCommitRedirectMetadataDocument,
|
||||
"\n query LegacyViewerStreamRedirectMetadata($streamId: String!) {\n project(id: $streamId) {\n id\n versions(limit: 1) {\n totalCount\n items {\n id\n model {\n id\n }\n }\n }\n }\n }\n": types.LegacyViewerStreamRedirectMetadataDocument,
|
||||
|
@ -665,7 +676,7 @@ export function graphql(source: "\n fragment ProjectsDashboardHeaderProjects_Us
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ProjectsDashboardHeaderWorkspaces_User on User {\n ...WorkspaceInviteBanners_User\n }\n"): (typeof documents)["\n fragment ProjectsDashboardHeaderWorkspaces_User on User {\n ...WorkspaceInviteBanners_User\n }\n"];
|
||||
export function graphql(source: "\n fragment ProjectsDashboardHeaderWorkspaces_User on User {\n discoverableWorkspaces {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace\n }\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n"): (typeof documents)["\n fragment ProjectsDashboardHeaderWorkspaces_User on User {\n discoverableWorkspaces {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace\n }\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -706,6 +717,18 @@ export function graphql(source: "\n fragment SettingsDialog_User on User {\n
|
|||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n"): (typeof documents)["\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n"): (typeof documents)["\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {\n id\n name\n description\n key\n }\n"): (typeof documents)["\n fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {\n id\n name\n description\n key\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {\n id\n name\n key\n description\n }\n"): (typeof documents)["\n fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {\n id\n name\n key\n description\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -745,7 +768,7 @@ export function graphql(source: "\n fragment UserProfileEditDialogAvatar_User o
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n billing {\n cost {\n subTotal\n total\n ...BillingSummary_WorkspaceCost\n }\n versionsCount {\n current\n max\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n billing {\n cost {\n subTotal\n total\n ...BillingSummary_WorkspaceCost\n }\n versionsCount {\n current\n max\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n id\n plan {\n name\n status\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n id\n plan {\n name\n status\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -770,6 +793,10 @@ export function graphql(source: "\n fragment SettingsWorkspacesMembers_Workspac
|
|||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n availableRegions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n availableRegions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -802,6 +829,10 @@ export function graphql(source: "\n fragment SettingsWorkspacesMembersMembersTa
|
|||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...WorkspaceInviteDialog_Workspace\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...WorkspaceInviteDialog_Workspace\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment SettingsWorkspacesRegionsSelect_ServerRegionItem on ServerRegionItem {\n id\n key\n name\n description\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesRegionsSelect_ServerRegionItem on ServerRegionItem {\n id\n key\n name\n description\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -837,7 +868,7 @@ export function graphql(source: "\n fragment WorkspaceAvatar_Workspace on Works
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n id\n team {\n items {\n id\n user {\n id\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n id\n team {\n items {\n id\n user {\n id\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n id\n team {\n items {\n id\n user {\n id\n role\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n id\n team {\n items {\n id\n user {\n id\n role\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -853,15 +884,11 @@ export function graphql(source: "\n fragment WorkspaceProjectList_ProjectCollec
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n slug\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n billing {\n versionsCount {\n ...WorkspacePageVersionCount_WorkspaceVersionsCount\n }\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n"): (typeof documents)["\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n slug\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n billing {\n versionsCount {\n ...WorkspacePageVersionCount_WorkspaceVersionsCount\n }\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n"];
|
||||
export function graphql(source: "\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n slug\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n"): (typeof documents)["\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n slug\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment WorkspaceInviteBanner_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n"): (typeof documents)["\n fragment WorkspaceInviteBanner_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment WorkspaceInviteBanners_User on User {\n discoverableWorkspaces {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace\n }\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n"): (typeof documents)["\n fragment WorkspaceInviteBanners_User on User {\n discoverableWorkspaces {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace\n }\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -869,11 +896,7 @@ export function graphql(source: "\n fragment WorkspaceInviteBlock_PendingWorksp
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace on DiscoverableWorkspace {\n id\n name\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace on DiscoverableWorkspace {\n id\n name\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment WorkspacePageVersionCount_WorkspaceVersionsCount on WorkspaceVersionsCount {\n current\n max\n }\n"): (typeof documents)["\n fragment WorkspacePageVersionCount_WorkspaceVersionsCount on WorkspaceVersionsCount {\n current\n max\n }\n"];
|
||||
export function graphql(source: "\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -957,7 +980,7 @@ export function graphql(source: "\n query UserSearch(\n $query: String!\n
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query ServerInfoBlobSizeLimit {\n serverInfo {\n blobSizeLimitBytes\n }\n }\n"): (typeof documents)["\n query ServerInfoBlobSizeLimit {\n serverInfo {\n blobSizeLimitBytes\n }\n }\n"];
|
||||
export function graphql(source: "\n query ServerInfoBlobSizeLimit {\n serverInfo {\n configuration {\n blobSizeLimitBytes\n }\n }\n }\n"): (typeof documents)["\n query ServerInfoBlobSizeLimit {\n serverInfo {\n configuration {\n blobSizeLimitBytes\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -969,7 +992,7 @@ export function graphql(source: "\n query ProjectModelsSelectorValues($projectI
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n"): (typeof documents)["\n query MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n"];
|
||||
export function graphql(source: "\n query MainServerInfoData {\n serverInfo {\n adminContact\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n"): (typeof documents)["\n query MainServerInfoData {\n serverInfo {\n adminContact\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -1046,6 +1069,14 @@ export function graphql(source: "\n subscription ProjectVersionGendoAIRenderCre
|
|||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n subscription ProjectVersionGendoAIRenderUpdated($id: String!, $versionId: String!) {\n projectVersionGendoAIRenderUpdated(id: $id, versionId: $versionId) {\n id\n projectId\n modelId\n versionId\n createdAt\n updatedAt\n gendoGenerationId\n status\n prompt\n camera\n responseImage\n }\n }\n"): (typeof documents)["\n subscription ProjectVersionGendoAIRenderUpdated($id: String!, $versionId: String!) {\n projectVersionGendoAIRenderUpdated(id: $id, versionId: $versionId) {\n id\n projectId\n modelId\n versionId\n createdAt\n updatedAt\n gendoGenerationId\n status\n prompt\n camera\n responseImage\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation CreateNewRegion($input: CreateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n create(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CreateNewRegion($input: CreateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n create(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation UpdateRegion($input: UpdateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n update(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateRegion($input: UpdateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n update(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -1094,6 +1125,10 @@ export function graphql(source: "\n mutation CreateModel($input: CreateModelInp
|
|||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation CreateProject($input: ProjectCreateInput) {\n projectMutations {\n create(input: $input) {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n"): (typeof documents)["\n mutation CreateProject($input: ProjectCreateInput) {\n projectMutations {\n create(input: $input) {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation CreateWorkspaceProject($input: WorkspaceProjectCreateInput!) {\n workspaceMutations {\n projects {\n create(input: $input) {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CreateWorkspaceProject($input: WorkspaceProjectCreateInput!) {\n workspaceMutations {\n projects {\n create(input: $input) {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -1438,6 +1473,18 @@ export function graphql(source: "\n query SettingsWorkspaceGeneral($id: String!
|
|||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query SettingsWorkspaceBilling($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspaceBilling($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query SettingsWorkspacePricingPlans {\n workspacePricingPlans\n }\n"): (typeof documents)["\n query SettingsWorkspacePricingPlans {\n workspacePricingPlans\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {\n workspace(id: $workspaceId) {\n customerPortalUrl\n }\n }\n"): (typeof documents)["\n query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {\n workspace(id: $workspaceId) {\n customerPortalUrl\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query SettingsWorkspaceRegions($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...SettingsWorkspacesRegions_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspaceRegions($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...SettingsWorkspacesRegions_Workspace\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -1586,6 +1633,10 @@ export function graphql(source: "\n mutation CreateWorkspace($input: WorkspaceC
|
|||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation ProcessWorkspaceInvite($input: WorkspaceInviteUseInput!) {\n workspaceMutations {\n invites {\n use(input: $input)\n }\n }\n }\n"): (typeof documents)["\n mutation ProcessWorkspaceInvite($input: WorkspaceInviteUseInput!) {\n workspaceMutations {\n invites {\n use(input: $input)\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation SetDefaultWorkspaceRegion($workspaceId: String!, $regionKey: String!) {\n workspaceMutations {\n setDefaultRegion(workspaceId: $workspaceId, regionKey: $regionKey) {\n id\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n }\n"): (typeof documents)["\n mutation SetDefaultWorkspaceRegion($workspaceId: String!, $regionKey: String!) {\n workspaceMutations {\n setDefaultRegion(workspaceId: $workspaceId, regionKey: $regionKey) {\n id\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -1606,6 +1657,10 @@ export function graphql(source: "\n query WorkspaceInvite(\n $workspaceId: S
|
|||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query MoveProjectsDialog {\n activeUser {\n ...MoveProjectsDialog_User\n }\n }\n"): (typeof documents)["\n query MoveProjectsDialog {\n activeUser {\n ...MoveProjectsDialog_User\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query ValidateWorkspaceSlug($slug: String!) {\n validateWorkspaceSlug(slug: $slug)\n }\n"): (typeof documents)["\n query ValidateWorkspaceSlug($slug: String!) {\n validateWorkspaceSlug(slug: $slug)\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -45,7 +45,9 @@ export const userSearchQuery = graphql(`
|
|||
export const serverInfoBlobSizeLimitQuery = graphql(`
|
||||
query ServerInfoBlobSizeLimit {
|
||||
serverInfo {
|
||||
blobSizeLimitBytes
|
||||
configuration {
|
||||
blobSizeLimitBytes
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
|
|
@ -13,6 +13,7 @@ export type MixpanelClient = Merge<
|
|||
| 'people'
|
||||
| 'add_group'
|
||||
| 'get_group'
|
||||
| 'alias'
|
||||
>,
|
||||
{
|
||||
people: Pick<OverridedMixpanel['people'], 'set' | 'set_once'>
|
||||
|
@ -33,5 +34,6 @@ export const fakeMixpanelClient = (): MixpanelClient => ({
|
|||
set_once: noop
|
||||
},
|
||||
add_group: noop,
|
||||
get_group: noop as MixpanelClient['get_group']
|
||||
get_group: noop as MixpanelClient['get_group'],
|
||||
alias: noop
|
||||
})
|
||||
|
|
|
@ -8,7 +8,6 @@ export const mainServerInfoDataQuery = graphql(`
|
|||
query MainServerInfoData {
|
||||
serverInfo {
|
||||
adminContact
|
||||
blobSizeLimitBytes
|
||||
canonicalUrl
|
||||
company
|
||||
description
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import type { Nullable } from '@speckle/shared'
|
||||
import { type LayoutDialogButton } from '@speckle/ui-components'
|
||||
|
||||
export type TutorialItem = {
|
||||
export type WebflowItem = {
|
||||
id: string
|
||||
readingTime?: number
|
||||
publishedAt?: Nullable<string>
|
||||
url?: string
|
||||
title?: string
|
||||
featureImage?: Nullable<string>
|
||||
title: string
|
||||
createdOn: string
|
||||
lastPublished: string
|
||||
featureImageUrl?: string
|
||||
url: string
|
||||
readTime?: number
|
||||
}
|
||||
|
||||
export type QuickStartItem = {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
// TODO: Clean up these operations and make them component fragment based. Also some of the props requested don't seem to even be used
|
||||
|
||||
export const requestGendoAIRender = graphql(`
|
||||
mutation requestGendoAIRender($input: GendoAIRenderInput!) {
|
||||
versionMutations {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export const useIsMultiregionEnabled = () => {
|
||||
const {
|
||||
public: { FF_WORKSPACES_MODULE_ENABLED, FF_WORKSPACES_MULTI_REGION_ENABLED }
|
||||
} = useRuntimeConfig()
|
||||
|
||||
return !!(FF_WORKSPACES_MODULE_ENABLED && FF_WORKSPACES_MULTI_REGION_ENABLED)
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { useMutation } from '@vue/apollo-composable'
|
||||
import type {
|
||||
CreateNewRegionMutationVariables,
|
||||
UpdateRegionMutationVariables
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { modifyObjectField, ROOT_QUERY } from '~/lib/common/helpers/graphql'
|
||||
import {
|
||||
createNewRegionMutation,
|
||||
updateRegionMutation
|
||||
} from '~/lib/multiregion/graphql/mutations'
|
||||
|
||||
export const useCreateRegion = () => {
|
||||
const { mutate } = useMutation(createNewRegionMutation)
|
||||
const { activeUser, isAdmin } = useActiveUser()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
return async (input: CreateNewRegionMutationVariables) => {
|
||||
if (!activeUser.value || !isAdmin.value) return
|
||||
|
||||
const res = await mutate(input, {
|
||||
update: (cache, { data }) => {
|
||||
const newRegion = data?.serverInfoMutations.multiRegion.create
|
||||
if (!newRegion) return
|
||||
|
||||
// Add to admin region list
|
||||
modifyObjectField(
|
||||
cache,
|
||||
ROOT_QUERY,
|
||||
'serverInfo',
|
||||
({ helpers: { createUpdatedValue, ref } }) =>
|
||||
createUpdatedValue(({ update }) => {
|
||||
update('multiRegion.regions', (regions) => [
|
||||
ref('ServerRegionItem', newRegion.id),
|
||||
...regions
|
||||
])
|
||||
})
|
||||
)
|
||||
}
|
||||
}).catch(convertThrowIntoFetchResult)
|
||||
|
||||
if (res?.data?.serverInfoMutations.multiRegion.create.id) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Success,
|
||||
title: 'Region successfully created'
|
||||
})
|
||||
} else {
|
||||
const errMsg = getFirstGqlErrorMessage(res?.errors)
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to create region',
|
||||
description: errMsg
|
||||
})
|
||||
}
|
||||
|
||||
return res?.data?.serverInfoMutations.multiRegion.create
|
||||
}
|
||||
}
|
||||
|
||||
export const useUpdateRegion = () => {
|
||||
const { mutate } = useMutation(updateRegionMutation)
|
||||
const { activeUser, isAdmin } = useActiveUser()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
return async (input: UpdateRegionMutationVariables) => {
|
||||
if (!activeUser.value || !isAdmin.value) return
|
||||
|
||||
const res = await mutate(input).catch(convertThrowIntoFetchResult)
|
||||
|
||||
if (res?.data?.serverInfoMutations.multiRegion.update.id) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Success,
|
||||
title: 'Region successfully updated'
|
||||
})
|
||||
} else {
|
||||
const errMsg = getFirstGqlErrorMessage(res?.errors)
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to update region',
|
||||
description: errMsg
|
||||
})
|
||||
}
|
||||
|
||||
return res?.data?.serverInfoMutations.multiRegion.update
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { graphql } from '~/lib/common/generated/gql'
|
||||
|
||||
export const createNewRegionMutation = graphql(`
|
||||
mutation CreateNewRegion($input: CreateServerRegionInput!) {
|
||||
serverInfoMutations {
|
||||
multiRegion {
|
||||
create(input: $input) {
|
||||
id
|
||||
...SettingsServerRegionsAddEditDialog_ServerRegionItem
|
||||
...SettingsServerRegionsTable_ServerRegionItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const updateRegionMutation = graphql(`
|
||||
mutation UpdateRegion($input: UpdateServerRegionInput!) {
|
||||
serverInfoMutations {
|
||||
multiRegion {
|
||||
update(input: $input) {
|
||||
id
|
||||
...SettingsServerRegionsAddEditDialog_ServerRegionItem
|
||||
...SettingsServerRegionsTable_ServerRegionItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
|
@ -21,7 +21,10 @@ import type {
|
|||
Workspace,
|
||||
WorkspaceProjectInviteCreateInput,
|
||||
InviteProjectUserMutation,
|
||||
Project
|
||||
Project,
|
||||
WorkspaceProjectCreateInput,
|
||||
CreateWorkspaceProjectMutation,
|
||||
CreateProjectMutation
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
ROOT_QUERY,
|
||||
|
@ -43,7 +46,8 @@ import {
|
|||
updateProjectRoleMutation,
|
||||
updateWorkspaceProjectRoleMutation,
|
||||
useProjectInviteMutation,
|
||||
useMoveProjectToWorkspaceMutation
|
||||
useMoveProjectToWorkspaceMutation,
|
||||
createWorkspaceProjectMutation
|
||||
} from '~~/lib/projects/graphql/mutations'
|
||||
import { onProjectUpdatedSubscription } from '~~/lib/projects/graphql/subscriptions'
|
||||
import { projectRoute } from '~/lib/common/helpers/route'
|
||||
|
@ -117,16 +121,32 @@ export function useCreateProject() {
|
|||
const { triggerNotification } = useGlobalToast()
|
||||
const { activeUser } = useActiveUser()
|
||||
|
||||
return async (input: ProjectCreateInput) => {
|
||||
return async (input: ProjectCreateInput | WorkspaceProjectCreateInput) => {
|
||||
const userId = activeUser.value?.id
|
||||
if (!userId) return
|
||||
|
||||
const res = await apollo
|
||||
.mutate({
|
||||
mutation: createProjectMutation,
|
||||
variables: { input },
|
||||
...('workspaceId' in input
|
||||
? {
|
||||
mutation: createWorkspaceProjectMutation,
|
||||
variables: { input }
|
||||
}
|
||||
: {
|
||||
mutation: createProjectMutation,
|
||||
variables: { input }
|
||||
}),
|
||||
update: (cache, { data }) => {
|
||||
const newProject = data?.projectMutations.create
|
||||
// not sure why this isn't happening automatically
|
||||
const typedData = data as
|
||||
| CreateWorkspaceProjectMutation
|
||||
| CreateProjectMutation
|
||||
|
||||
if (!typedData) return
|
||||
const newProject =
|
||||
'projectMutations' in typedData
|
||||
? typedData.projectMutations.create
|
||||
: typedData.workspaceMutations.projects.create
|
||||
|
||||
if (newProject?.id) {
|
||||
// Existing cache update for projects
|
||||
|
@ -136,7 +156,7 @@ export function useCreateProject() {
|
|||
>(
|
||||
cache,
|
||||
ROOT_QUERY,
|
||||
(fieldName, _variables, value, details) => {
|
||||
(_fieldName, _variables, value, details) => {
|
||||
const projectListFields = Object.keys(value).filter(
|
||||
(k) =>
|
||||
details.revolveFieldNameAndVariables(k).fieldName === 'projectList'
|
||||
|
@ -150,7 +170,7 @@ export function useCreateProject() {
|
|||
{ fieldNameWhitelist: ['admin'] }
|
||||
)
|
||||
|
||||
if (input.workspaceId) {
|
||||
if ('workspaceId' in input && input.workspaceId) {
|
||||
const workspaceCacheId = getCacheId('Workspace', input.workspaceId)
|
||||
|
||||
modifyObjectFields<WorkspaceProjectsArgs, Workspace['projects']>(
|
||||
|
@ -181,7 +201,18 @@ export function useCreateProject() {
|
|||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
|
||||
if (!res.data?.projectMutations.create.id) {
|
||||
// not sure why this isn't happening automatically
|
||||
const typedData = res.data as Optional<
|
||||
CreateWorkspaceProjectMutation | CreateProjectMutation
|
||||
>
|
||||
|
||||
const newProject = typedData
|
||||
? 'projectMutations' in typedData
|
||||
? typedData.projectMutations.create
|
||||
: typedData.workspaceMutations.projects.create
|
||||
: undefined
|
||||
|
||||
if (!newProject?.id) {
|
||||
const err = getFirstErrorMessage(res.errors)
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
|
@ -195,7 +226,7 @@ export function useCreateProject() {
|
|||
})
|
||||
}
|
||||
|
||||
return res
|
||||
return newProject
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -350,7 +350,6 @@ export function useDeleteVersions() {
|
|||
* Various options for better cache updates, set if possible
|
||||
*/
|
||||
options?: Partial<{
|
||||
projectId: string
|
||||
modelId: string
|
||||
}>
|
||||
) => {
|
||||
|
@ -372,26 +371,21 @@ export function useDeleteVersions() {
|
|||
}
|
||||
|
||||
// Update totalCounts in project
|
||||
if (options?.projectId) {
|
||||
modifyObjectFields<ProjectVersionsArgs, Project['versions']>(
|
||||
cache,
|
||||
getCacheId('Project', options.projectId),
|
||||
(_fieldName, _variables, data) => {
|
||||
return {
|
||||
...data,
|
||||
...(!isUndefined(data.totalCount)
|
||||
? {
|
||||
totalCount: Math.max(
|
||||
data.totalCount - input.versionIds.length,
|
||||
0
|
||||
)
|
||||
}
|
||||
: {})
|
||||
}
|
||||
},
|
||||
{ fieldNameWhitelist: ['versions'] }
|
||||
)
|
||||
}
|
||||
modifyObjectFields<ProjectVersionsArgs, Project['versions']>(
|
||||
cache,
|
||||
getCacheId('Project', input.projectId),
|
||||
(_fieldName, _variables, data) => {
|
||||
return {
|
||||
...data,
|
||||
...(!isUndefined(data.totalCount)
|
||||
? {
|
||||
totalCount: Math.max(data.totalCount - input.versionIds.length, 0)
|
||||
}
|
||||
: {})
|
||||
}
|
||||
},
|
||||
{ fieldNameWhitelist: ['versions'] }
|
||||
)
|
||||
|
||||
// Update totalCounts in model
|
||||
if (options?.modelId) {
|
||||
|
@ -458,7 +452,6 @@ export function useMoveVersions() {
|
|||
options?: Partial<{
|
||||
previousModelId: string
|
||||
newModelCreated: boolean
|
||||
projectId: string
|
||||
}>
|
||||
) => {
|
||||
if (!input.versionIds.length || !input.targetModelName.trim()) return
|
||||
|
@ -551,8 +544,8 @@ export function useMoveVersions() {
|
|||
{ fieldNameWhitelist: ['versions'] }
|
||||
)
|
||||
|
||||
if (options?.newModelCreated && options?.projectId) {
|
||||
evictProjectModels(options.projectId)
|
||||
if (options?.newModelCreated) {
|
||||
evictProjectModels(input.projectId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -21,6 +21,19 @@ export const createProjectMutation = graphql(`
|
|||
}
|
||||
`)
|
||||
|
||||
export const createWorkspaceProjectMutation = graphql(`
|
||||
mutation CreateWorkspaceProject($input: WorkspaceProjectCreateInput!) {
|
||||
workspaceMutations {
|
||||
projects {
|
||||
create(input: $input) {
|
||||
...ProjectPageProject
|
||||
...ProjectDashboardItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const updateModelMutation = graphql(`
|
||||
mutation UpdateModel($input: UpdateModelInput!) {
|
||||
modelMutations {
|
||||
|
|
|
@ -4,6 +4,7 @@ import SettingsUserNotifications from '~/components/settings/user/Notifications.
|
|||
import SettingsUserDeveloper from '~/components/settings/user/developer/Developer.vue'
|
||||
import SettingsUserEmails from '~/components/settings/user/Emails.vue'
|
||||
import SettingsServerGeneral from '~/components/settings/server/General.vue'
|
||||
import SettingsServerRegions from '~/components/settings/server/Regions.vue'
|
||||
import SettingsServerProjects from '~/components/settings/server/Projects.vue'
|
||||
import SettingsServerMembers from '~/components/settings/server/Members.vue'
|
||||
import SettingsWorkspaceGeneral from '~/components/settings/workspaces/General.vue'
|
||||
|
@ -11,11 +12,16 @@ import SettingsWorkspacesMembers from '~/components/settings/workspaces/Members.
|
|||
import SettingsWorkspacesSecurity from '~/components/settings/workspaces/Security.vue'
|
||||
import SettingsWorkspacesProjects from '~/components/settings/workspaces/Projects.vue'
|
||||
import SettingsWorkspacesBilling from '~/components/settings/workspaces/Billing.vue'
|
||||
import SettingsWorkspacesRegions from '~/components/settings/workspaces/Regions.vue'
|
||||
import { useIsMultipleEmailsEnabled } from '~/composables/globals'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { SettingMenuKeys } from '~/lib/settings/helpers/types'
|
||||
import { useIsMultiregionEnabled } from '~/lib/multiregion/composables/main'
|
||||
|
||||
export const useSettingsMenu = () => {
|
||||
const isMultipleEmailsEnabled = useIsMultipleEmailsEnabled().value
|
||||
const isMultiRegionEnabled = useIsMultiregionEnabled()
|
||||
|
||||
const workspaceMenuItems = shallowRef<SettingsMenuItems>({
|
||||
[SettingMenuKeys.Workspace.General]: {
|
||||
title: 'General',
|
||||
|
@ -44,29 +50,30 @@ export const useSettingsMenu = () => {
|
|||
},
|
||||
[SettingMenuKeys.Workspace.Regions]: {
|
||||
title: 'Regions',
|
||||
disabled: true,
|
||||
tooltipText: 'Set up regions for custom data residency',
|
||||
permission: [Roles.Workspace.Admin, Roles.Workspace.Member]
|
||||
component: SettingsWorkspacesRegions,
|
||||
permission: [Roles.Workspace.Admin, Roles.Workspace.Member],
|
||||
...(isMultiRegionEnabled
|
||||
? {}
|
||||
: {
|
||||
tooltipText: 'Set up regions for custom data residency',
|
||||
disabled: true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const multipleEmailsEnabled = useIsMultipleEmailsEnabled().value
|
||||
|
||||
const userMenuItemValues: SettingsMenuItems = {
|
||||
const userMenuItems = shallowRef<SettingsMenuItems>({
|
||||
[SettingMenuKeys.User.Profile]: {
|
||||
title: 'User profile',
|
||||
component: SettingsUserProfile
|
||||
}
|
||||
}
|
||||
|
||||
if (multipleEmailsEnabled) {
|
||||
userMenuItemValues[SettingMenuKeys.User.Emails] = {
|
||||
title: 'Emails',
|
||||
component: SettingsUserEmails
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(userMenuItemValues, {
|
||||
},
|
||||
...(isMultipleEmailsEnabled
|
||||
? {
|
||||
[SettingMenuKeys.User.Emails]: {
|
||||
title: 'Emails',
|
||||
component: SettingsUserEmails
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
[SettingMenuKeys.User.Notifications]: {
|
||||
title: 'Notifications',
|
||||
component: SettingsUserNotifications
|
||||
|
@ -77,8 +84,6 @@ export const useSettingsMenu = () => {
|
|||
}
|
||||
})
|
||||
|
||||
const userMenuItems = shallowRef<SettingsMenuItems>(userMenuItemValues)
|
||||
|
||||
const serverMenuItems = shallowRef<SettingsMenuItems>({
|
||||
[SettingMenuKeys.Server.General]: {
|
||||
title: 'General',
|
||||
|
@ -91,7 +96,15 @@ export const useSettingsMenu = () => {
|
|||
[SettingMenuKeys.Server.Projects]: {
|
||||
title: 'Projects',
|
||||
component: SettingsServerProjects
|
||||
}
|
||||
},
|
||||
...(isMultiRegionEnabled
|
||||
? {
|
||||
[SettingMenuKeys.Server.Regions]: {
|
||||
title: 'Regions',
|
||||
component: SettingsServerRegions
|
||||
}
|
||||
}
|
||||
: {})
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
@ -25,6 +25,29 @@ export const settingsWorkspaceBillingQuery = graphql(`
|
|||
}
|
||||
`)
|
||||
|
||||
export const settingsWorkspacePricingPlansQuery = graphql(`
|
||||
query SettingsWorkspacePricingPlans {
|
||||
workspacePricingPlans
|
||||
}
|
||||
`)
|
||||
|
||||
export const settingsWorkspaceBillingCustomerPortalQuery = graphql(`
|
||||
query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {
|
||||
workspace(id: $workspaceId) {
|
||||
customerPortalUrl
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const settingsWorkspaceRegionsQuery = graphql(`
|
||||
query SettingsWorkspaceRegions($workspaceId: String!) {
|
||||
workspace(id: $workspaceId) {
|
||||
id
|
||||
...SettingsWorkspacesRegions_Workspace
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const settingsWorkspacesMembersQuery = graphql(`
|
||||
query SettingsWorkspacesMembers(
|
||||
$workspaceId: String!
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import type { AvailableRoles } from '@speckle/shared'
|
||||
import { isObjectLike, has } from 'lodash'
|
||||
import type { WorkspacePlans } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
export type SettingsMenuItem = {
|
||||
title: string
|
||||
|
@ -23,7 +25,8 @@ export const SettingMenuKeys = Object.freeze(<const>{
|
|||
General: 'server/general',
|
||||
Projects: 'server/projects',
|
||||
ActiveUsers: 'server/active-users',
|
||||
PendingInvitations: 'server/pending-invitations'
|
||||
PendingInvitations: 'server/pending-invitations',
|
||||
Regions: 'server/regions'
|
||||
},
|
||||
Workspace: {
|
||||
General: 'workspace/general',
|
||||
|
@ -46,3 +49,22 @@ export type AvailableSettingsMenuKeys =
|
|||
| UserSettingMenuKeys
|
||||
| ServerSettingMenuKeys
|
||||
| WorkspaceSettingMenuKeys
|
||||
|
||||
export type WorkspacePricingPlans = {
|
||||
workspacePricingPlans: {
|
||||
workspacePlanInformation: {
|
||||
[key: string]: {
|
||||
name: WorkspacePlans
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isWorkspacePricingPlans(
|
||||
pricingPlans: unknown
|
||||
): pricingPlans is WorkspacePricingPlans {
|
||||
return (
|
||||
isObjectLike(pricingPlans) &&
|
||||
has(pricingPlans, 'workspacePricingPlans.workspacePlanInformation')
|
||||
)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export const items = [
|
|||
}
|
||||
},
|
||||
{
|
||||
camPos: [-39.91711, 46.26069, 42.83686, -18.44162, 29.75982, 34.91624, 0, 1],
|
||||
camPos: [23.86779, 82.9541, 29.05586, -27.41942, 37.72358, 29.05586, 0, 1],
|
||||
style: {} as Partial<CSSProperties>,
|
||||
viewed: false,
|
||||
showControls: false,
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче