diff --git a/.circleci/common.sh b/.circleci/common.sh index cea7aba96..e175fbe9c 100755 --- a/.circleci/common.sh +++ b/.circleci/common.sh @@ -10,6 +10,6 @@ LAST_RELEASE="$(git describe --always --tags $(git rev-list --tags) | grep -E '^ # shellcheck disable=SC2034 NEXT_RELEASE="$(echo "${LAST_RELEASE}" | awk -F. -v OFS=. '{$NF += 1 ; print}')" # shellcheck disable=SC2034 -BRANCH_NAME_TRUNCATED="$(echo "${CIRCLE_BRANCH}" | cut -c -50 | sed 's/[^a-zA-Z0-9_.-]/_/g')" # docker has a 128 character tag limit, so ensuring the branch name will be short enough +BRANCH_NAME_TRUNCATED="$(echo "${CIRCLE_BRANCH}" | cut -c -50 | sed 's/[^a-zA-Z0-9.-]/-/g')" # docker has a 128 character tag limit, so ensuring the branch name will be short enough # shellcheck disable=SC2034 COMMIT_SHA1_TRUNCATED="$(echo "${CIRCLE_SHA1}" | cut -c -7)" diff --git a/.circleci/config.yml b/.circleci/config.yml index b65b2e640..d6cc3bb50 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,6 +4,24 @@ orbs: snyk: snyk/snyk@2.0.3 codecov: codecov/codecov@5.0.3 +aliases: + - &docker-base-image + docker: + - image: cimg/base:2024.02 + + - &docker-node-image + docker: + - image: cimg/node:18.19.0 + + - &docker-node-image-w-browsers + docker: + - image: cimg/node:18.19.0-browsers + + - &yarn + run: + name: Install Dependencies + command: PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn + workflows: test-build: jobs: @@ -217,6 +235,7 @@ workflows: filters: *filters-publish requires: - get-version + - publish-approval - docker-publish-webhooks: context: *docker-hub-context @@ -380,8 +399,7 @@ workflows: jobs: get-version: - docker: &docker-base-image - - image: cimg/base:2024.02 + <<: *docker-base-image working_directory: &work-dir /tmp/ci steps: - checkout @@ -430,15 +448,7 @@ jobs: key: cache-pre-commit-<>-{{ checksum "<>" }} paths: - ~/.cache/pre-commit - - run: - name: Install Dependencies - command: yarn - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged + - *yarn - run: name: Build public packages command: yarn build:public @@ -487,63 +497,47 @@ jobs: RATELIMITER_ENABLED: 'false' steps: - checkout - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-server-{{ checksum "yarn.lock" }} - - run: - name: Install Dependencies - command: yarn - - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-server-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged - + - *yarn - run: name: Build public packages command: yarn build:public - - run: name: Wait for dependencies to start command: 'dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m' - - run: - command: touch .env.test + command: cp .env.test-example .env.test working_directory: 'packages/server' - - run: name: 'Lint' command: yarn lint:ci working_directory: 'packages/server' - - run: name: 'Run tests' # Extra formatting to get timestamps on each line in CI (for profiling purposes) - command: yarn test:report --color=always | while IFS= read -r line; do echo -e "$(date +%T.%3N) > $line"; done + command: | + GREP_FLAG="" + + if [ "$RUN_TESTS_IN_MULTIREGION_MODE" == "true" ]; then + GREP_FLAG="--grep @multiregion" + fi + + yarn test:report $GREP_FLAG --color=always | while IFS= read -r line; do echo -e "$(date +%T.%3N) > $line"; done working_directory: 'packages/server' no_output_timeout: 30m - - codecov/upload: files: packages/server/coverage/lcov.info - - run: name: Introspect GQL schema for subsequent checks command: 'IGNORE_MISSING_MIRATIONS=true yarn cli graphql introspect' working_directory: 'packages/server' - - run: name: Checking for GQL schema breakages against app.speckle.systems command: 'yarn rover graph check Speckle-Server@app-speckle-systems --schema ./introspected-schema.graphql' working_directory: 'packages/server' - - run: name: Checking for GQL schema breakages against latest.speckle.systems command: 'yarn rover graph check Speckle-Server@latest-speckle-systems --schema ./introspected-schema.graphql' working_directory: 'packages/server' - - store_test_results: path: packages/server/reports @@ -631,68 +625,44 @@ jobs: RATELIMITER_ENABLED: 'false' test-frontend-2: - docker: &docker-node-browsers-image - - image: cimg/node:22.6.0-browsers + <<: *docker-node-image-w-browsers resource_class: xlarge steps: - checkout - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-server-{{ checksum "yarn.lock" }} - - run: - name: Install Dependencies - command: yarn - - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-server-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged - + - *yarn - run: name: Build public packages command: yarn build:public - - run: name: Lint everything command: yarn lint:ci working_directory: 'packages/frontend-2' test-viewer: - docker: *docker-node-browsers-image + <<: *docker-node-image-w-browsers resource_class: large steps: - checkout - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-server-{{ checksum "yarn.lock" }} - - run: - name: Install Dependencies - command: yarn - - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-server-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged - + - *yarn - run: name: Build public packages command: yarn build:public - - run: - name: Lint everything + name: Lint viewer command: yarn lint:ci working_directory: 'packages/viewer' - - run: name: Run tests command: yarn test working_directory: 'packages/viewer' + - run: + name: Lint viewer-sandbox + command: yarn lint:ci + working_directory: 'packages/viewer-sandbox' + - run: + name: Build viewer-sandbox + command: yarn build + working_directory: 'packages/viewer-sandbox' test-preview-service: docker: @@ -706,92 +676,60 @@ jobs: environment: {} steps: - checkout - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-server-{{ checksum "yarn.lock" }} - - run: - name: Install Dependencies - command: yarn - - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-server-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged - + - *yarn - run: name: Build public packages command: yarn build:public - - run: name: Lint everything command: yarn lint:ci working_directory: 'packages/preview-service' - - run: name: Copy .env.example to .env command: | #!/usr/bin/env bash cp packages/preview-service/.env.example packages/preview-service/.env sed -i~ '/^PG_CONNECTION_STRING=/s/=.*/="postgres:\/\/preview_service_test:preview_service_test@127.0.0.1:5432\/preview_service_test"/' packages/preview-service/.env - - run: name: Run tests command: yarn test working_directory: 'packages/preview-service' test-shared: - docker: *docker-node-browsers-image + <<: *docker-node-image-w-browsers resource_class: medium+ steps: - checkout - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-server-{{ checksum "yarn.lock" }} - - run: - name: Install Dependencies - command: yarn - - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-server-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged - + - *yarn - run: name: Lint command: yarn lint:ci working_directory: 'packages/shared' - - run: name: Run tests - command: yarn test:single-run + command: yarn test:ci + working_directory: 'packages/shared' + - codecov/upload: + files: packages/shared/coverage/coverage-final.json + - run: + name: Build + command: yarn build + working_directory: 'packages/shared' + - run: + name: Ensure ESM import works + command: node ./e2e/testEsm.mjs + working_directory: 'packages/shared' + - run: + name: Ensure CJS require works + command: node ./e2e/testCjs.cjs working_directory: 'packages/shared' test-objectsender: - docker: *docker-node-browsers-image + <<: *docker-node-image-w-browsers resource_class: large steps: - checkout - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-server-{{ checksum "yarn.lock" }} - - run: - name: Install Dependencies - command: yarn - - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-server-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged - + - *yarn - run: name: Build public packages command: yarn build:public @@ -803,78 +741,42 @@ jobs: path: 'packages/objectsender/coverage' test-ui-components: - docker: *docker-node-browsers-image + <<: *docker-node-image-w-browsers resource_class: xlarge steps: - checkout - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-server-{{ checksum "yarn.lock" }} - run: name: Install Dependencies - command: yarn - - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-server-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged - + command: PUPPETEER_SKIP_DOWNLOAD=true yarn - run: name: Build public packages command: yarn build:public - - run: name: Lint tailwind theme command: yarn lint:ci working_directory: 'packages/tailwind-theme' - - run: name: Lint ui components command: yarn lint:ci working_directory: 'packages/ui-components' - - run: name: Lint component nuxt package command: yarn lint:ci working_directory: 'packages/ui-components-nuxt' - - - run: - name: Install Playwright - command: cd ~ && npx playwright install --with-deps - - run: name: Test via Storybook command: yarn storybook:test:ci working_directory: 'packages/ui-components' ui-components-chromatic: + <<: *docker-node-image resource_class: medium+ - docker: &docker-node-image - - image: cimg/node:22.6.0 steps: - checkout - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-server-{{ checksum "yarn.lock" }} - - run: - name: Install Dependencies - command: yarn - - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-server-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged - + - *yarn - run: name: Build shared packages command: yarn build:public - - run: name: Run chromatic command: yarn chromatic @@ -886,24 +788,12 @@ jobs: # but it is not possible to scan npm/yarn package.json # because it requires node_modules # therefore this scanning has to be triggered via the cli - docker: *docker-node-image + <<: *docker-node-image resource_class: medium working_directory: *work-dir steps: - checkout - - restore_cache: - name: Restore Yarn Package cache - keys: - - yarn-packages-server-{{ checksum "yarn.lock" }} - - run: - name: Install Dependencies - command: yarn - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-server-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged + - *yarn - snyk/scan: additional-arguments: --yarn-workspaces --strict-out-of-sync=false fail-on-issues: false @@ -979,7 +869,7 @@ jobs: - run: echo "export KUBECONFIG=$(pwd)/.kube/config" >> "${BASH_ENV}" - run: echo "${KUBECONFIG}" - run: - name: Template Helm Chart + name: Template Speckle Server Helm Chart command: | nix-shell \ --run "helm template speckle-server ./utils/helm/speckle-server" \ @@ -1002,14 +892,12 @@ jobs: echo "🔐 We need 'sudo' to set permissions on postgres-data directory to 775" sudo chmod 775 "./postgres-data" fi - - run: name: Deploy Kubernetes (kind) cluster command: | nix-shell \ --run "ctlptl apply --filename ./.circleci/deployment/cluster-config.yaml" \ ./.circleci/deployment/helm-chart-shell.nix - - run: name: Deploy Kubernetes resources to cluster command: | @@ -1018,7 +906,7 @@ jobs: ./.circleci/deployment/helm-chart-shell.nix docker-build: &build-job - docker: *docker-base-image + <<: *docker-base-image resource_class: medium working_directory: *work-dir steps: @@ -1049,7 +937,7 @@ jobs: SPECKLE_SERVER_PACKAGE: frontend-2 docker-publish-frontend-2-sourcemaps: - docker: *docker-node-image + <<: *docker-node-image resource_class: xlarge working_directory: *work-dir environment: @@ -1105,7 +993,7 @@ jobs: SPECKLE_SERVER_PACKAGE: docker-compose-ingress docker-publish: &publish-job - docker: *docker-base-image + <<: *docker-base-image resource_class: medium working_directory: *work-dir steps: @@ -1169,26 +1057,14 @@ jobs: SPECKLE_SERVER_PACKAGE: docker-compose-ingress publish-npm: - docker: *docker-node-image + <<: *docker-node-image working_directory: *work-dir steps: - checkout - attach_workspace: at: /tmp/ci/workspace - run: cat workspace/env-vars >> $BASH_ENV - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-{{ checksum "yarn.lock" }} - - run: - name: Install Dependencies - command: yarn - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged + - *yarn - run: name: auth to npm as Speckle command: | @@ -1197,7 +1073,6 @@ jobs: - run: name: try login to npm command: yarn npm whoami - - run: name: build public packages command: yarn workspaces foreach -ptvW --no-private run build @@ -1205,7 +1080,6 @@ jobs: name: bump all versions # bump all versions in dependency tree order but not in parallel command: yarn workspaces foreach -tvW version $IMAGE_VERSION_TAG - - run: name: publish to npm command: 'yarn workspaces foreach -pvW --no-private npm publish --access public' @@ -1227,7 +1101,7 @@ jobs: command: ./.circleci/publish_helm_chart.sh update-helm-documentation: - docker: *docker-node-image + <<: *docker-node-image working_directory: *work-dir steps: - checkout @@ -1242,24 +1116,12 @@ jobs: command: ./.circleci/update_helm_documentation.sh publish-viewer-sandbox-cloudflare-pages: - docker: *docker-node-image + <<: *docker-node-image working_directory: *work-dir resource_class: large steps: - checkout - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-server-{{ checksum "yarn.lock" }} - - run: - name: Install Dependencies - command: yarn - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-server-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - .yarn/unplugged + - *yarn - run: name: Build public packages command: yarn build:public diff --git a/.circleci/get_version.sh b/.circleci/get_version.sh index 1c89c2b4b..e4c28f24c 100755 --- a/.circleci/get_version.sh +++ b/.circleci/get_version.sh @@ -15,5 +15,11 @@ if [[ "${CIRCLE_BRANCH}" == "main" ]]; then exit 0 fi +# if branch name truncated contains an underscore, we should exit +if [[ "${BRANCH_NAME_TRUNCATED}" =~ "_" ]]; then + echo "Branch name contains an underscore, exiting" + exit 1 +fi + echo "${NEXT_RELEASE}-branch.${BRANCH_NAME_TRUNCATED}.${CIRCLE_BUILD_NUM}-${COMMIT_SHA1_TRUNCATED}" exit 0 diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh index 7b8817014..5074ca4d5 100755 --- a/.devcontainer/postCreateCommand.sh +++ b/.devcontainer/postCreateCommand.sh @@ -13,5 +13,5 @@ cp -n "${GIT_ROOT}/packages/server/.env-example" "${GIT_ROOT}/packages/server/.e cp -n "${GIT_ROOT}/packages/frontend-2/.env.example" "${GIT_ROOT}/packages/frontend-2/.env" || true echo "Installing nodejs dependencies and building shared packages" -yarn +PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn yarn build:public diff --git a/.gitignore b/.gitignore index 97264aa4d..42c6d98b8 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,8 @@ minio-data/ postgres-data/ redis-data/ +packages/fileimport-service/src/ifc-dotnet/output + .tshy-build obj/ bin/ @@ -76,6 +78,7 @@ bin/ !packages/monitor-deployment/bin !packages/preview-service/bin !packages/server/bin +!packages/viewer/src/modules/loaders/OBJ # Server multiregion.json diff --git a/.prettierignore b/.prettierignore index a7733da94..242072ef1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,7 +6,7 @@ dist-* coverage .nyc_output packages/server/reports* -packages/preview-service/public/render/**/* +packages/preview-service/public/**/* packages/objectloader/examples/browser/objectloader.web.js packages/viewer/example/speckleviewer.web.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..141b90cb6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:3033", + "webRoot": "${workspaceFolder}" + } + ] +} diff --git a/.yarnrc.yml b/.yarnrc.yml index 78e561bb6..61defe11c 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -2,6 +2,8 @@ compressionLevel: mixed enableGlobalCache: false +enableMirror: false + nodeLinker: node-modules yarnPath: .yarn/releases/yarn-4.5.0.cjs diff --git a/README.md b/README.md index e1fa910b0..b9753c875 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Have you checked our [dev docs](https://speckle.guide/dev/)? We have a detailed section on [deploying a Speckle server](https://speckle.guide/dev/server-setup.html). To get started developing locally, you can see the [Local development environment](https://speckle.guide/dev/server-local-dev.html) page. -## TL;DR +## TL;DR; We're using yarn and its workspaces functionalities to manage the monorepo. Make sure you are using [Node](https://nodejs.org/en) version 22. diff --git a/codecov.yml b/codecov.yml index 9ebf3bec0..24abb6c9a 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,3 +2,38 @@ codecov: notify: notify_error: true require_ci_to_pass: false + +coverage: + status: + project: + default: + target: 90% #overall project/ repo coverage + server: + target: 70% + flags: + - server + shared: + target: 70% + flags: + - shared + patch: + default: + target: 90% #overall project/ repo coverage + server: + target: 90% + flags: + - server + shared: + target: 100% + flags: + - shared + +flags: + server: + paths: + - packages/server/coverage/lcov.info + carryforward: false + shared: + paths: + - packages/shared/coverage/coverage-final.json + carryforward: false diff --git a/package.json b/package.json index 832d380ab..156b4f08e 100644 --- a/package.json +++ b/package.json @@ -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:down:volumes": "docker compose -f ./docker-compose-deps.yml down --volumes", "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", @@ -38,7 +39,8 @@ "postinstall": "husky install", "cm": "cz", "eslint:inspect": "eslint-config-inspector", - "eslint:projectwide": "node ./utils/eslint-projectwide.mjs" + "eslint:projectwide": "node ./utils/eslint-projectwide.mjs", + "npkill": "npkill" }, "devDependencies": { "@eslint/config-inspector": "^0.4.10", diff --git a/packages/dui3/lib/common/generated/gql/graphql.ts b/packages/dui3/lib/common/generated/gql/graphql.ts index 41045642b..a30bbdd95 100644 --- a/packages/dui3/lib/common/generated/gql/graphql.ts +++ b/packages/dui3/lib/common/generated/gql/graphql.ts @@ -27,6 +27,7 @@ export type ActiveUserMutations = { emailMutations: UserEmailMutations; /** Mark onboarding as complete */ finishOnboarding: Scalars['Boolean']['output']; + meta: UserMetaMutations; setActiveWorkspace: Scalars['Boolean']['output']; /** Edit a user's profile */ update: User; @@ -361,8 +362,10 @@ export type AutomateFunctionToken = { }; export type AutomateFunctionsFilter = { - /** By default we skip functions without releases. Set this to true to include them. */ - functionsWithoutReleases?: InputMaybe; + /** By default, we include featured ("public") functions. Set this to false to exclude them. */ + includeFeatured?: InputMaybe; + /** By default, we exclude functions without releases. Set this to false to include them. */ + requireRelease?: InputMaybe; search?: InputMaybe; }; @@ -432,6 +435,7 @@ export type Automation = { id: Scalars['ID']['output']; isTestAutomation: Scalars['Boolean']['output']; name: Scalars['String']['output']; + permissions: AutomationPermissionChecks; runs: AutomateRunCollection; updatedAt: Scalars['DateTime']['output']; }; @@ -449,6 +453,13 @@ export type AutomationCollection = { totalCount: Scalars['Int']['output']; }; +export type AutomationPermissionChecks = { + __typename?: 'AutomationPermissionChecks'; + canDelete: PermissionCheckResult; + canRead: PermissionCheckResult; + canUpdate: PermissionCheckResult; +}; + export type AutomationRevision = { __typename?: 'AutomationRevision'; functions: Array; @@ -588,6 +599,7 @@ export type CheckoutSession = { export type CheckoutSessionInput = { billingInterval: BillingInterval; + currency?: InputMaybe; isCreateFlow?: InputMaybe; workspaceId: Scalars['ID']['input']; workspacePlan: PaidWorkspacePlans; @@ -609,8 +621,9 @@ export type Comment = { id: Scalars['String']['output']; /** Parent thread, if there's any */ parent?: Maybe; + permissions: CommentPermissionChecks; /** Plain-text version of the comment text, ideal for previews */ - rawText: Scalars['String']['output']; + rawText?: Maybe; /** @deprecated Not actually implemented */ reactions?: Maybe>>; /** Gets the replies to this comment. */ @@ -620,7 +633,7 @@ export type Comment = { /** Resources that this comment targets. Can be a mixture of either one stream, or multiple commits and objects. */ resources: Array; screenshot?: Maybe; - text: SmartTextEditorValue; + text?: Maybe; /** The time this comment was last updated. Corresponds also to the latest reply to this comment, if any. */ updatedAt: Scalars['DateTime']['output']; /** The last time you viewed this comment. Present only if an auth'ed request. Relevant only if a top level commit. */ @@ -742,6 +755,11 @@ export type CommentMutationsReplyArgs = { input: CreateCommentReplyInput; }; +export type CommentPermissionChecks = { + __typename?: 'CommentPermissionChecks'; + canArchive: PermissionCheckResult; +}; + export type CommentReplyAuthorCollection = { __typename?: 'CommentReplyAuthorCollection'; items: Array; @@ -926,6 +944,17 @@ export type CreateVersionInput = { totalChildrenCount?: InputMaybe; }; +export enum Currency { + Gbp = 'gbp', + Usd = 'usd' +} + +export type CurrencyBasedPrices = { + __typename?: 'CurrencyBasedPrices'; + gbp: WorkspacePaidPlanPrices; + usd: WorkspacePaidPlanPrices; +}; + export type DeleteModelInput = { id: Scalars['ID']['input']; projectId: Scalars['ID']['input']; @@ -1041,6 +1070,10 @@ export type GendoAiRenderInput = { versionId: Scalars['ID']['input']; }; +export type InvitableCollaboratorsFilter = { + search?: InputMaybe; +}; + export type JoinWorkspaceInput = { workspaceId: Scalars['ID']['input']; }; @@ -1155,7 +1188,7 @@ export type LimitedUserTimelineArgs = { * to another user */ export type LimitedUserWorkspaceDomainPolicyCompliantArgs = { - workspaceId?: InputMaybe; + workspaceSlug?: InputMaybe; }; @@ -1236,6 +1269,7 @@ export type Model = { name: Scalars['String']['output']; /** Returns a list of versions that are being created from a file import */ pendingImportedVersions: Array; + permissions: ModelPermissionChecks; previewUrl?: Maybe; updatedAt: Scalars['DateTime']['output']; version: Version; @@ -1294,6 +1328,13 @@ export type ModelMutationsUpdateArgs = { input: UpdateModelInput; }; +export type ModelPermissionChecks = { + __typename?: 'ModelPermissionChecks'; + canCreateVersion: PermissionCheckResult; + canDelete: PermissionCheckResult; + canUpdate: PermissionCheckResult; +}; + export type ModelVersionsFilter = { /** Make sure these specified versions are always loaded first */ priorityIds?: InputMaybe>; @@ -1872,11 +1913,10 @@ export type OnboardingCompletionInput = { }; export enum PaidWorkspacePlans { - Business = 'business', - Plus = 'plus', Pro = 'pro', - Starter = 'starter', - Team = 'team' + ProUnlimited = 'proUnlimited', + Team = 'team', + TeamUnlimited = 'teamUnlimited' } export type PasswordStrengthCheckFeedback = { @@ -1951,6 +1991,14 @@ export type PendingWorkspaceCollaboratorsFilter = { search?: InputMaybe; }; +export type PermissionCheckResult = { + __typename?: 'PermissionCheckResult'; + authorized: Scalars['Boolean']['output']; + code: Scalars['String']['output']; + message: Scalars['String']['output']; + payload?: Maybe; +}; + export type Price = { __typename?: 'Price'; amount: Scalars['Float']['output']; @@ -1974,6 +2022,7 @@ export type Project = { createdAt: Scalars['DateTime']['output']; description?: Maybe; id: Scalars['ID']['output']; + invitableCollaborators: WorkspaceCollaboratorCollection; /** Collaborators who have been invited, but not yet accepted. */ invitedTeam?: Maybe>; /** Returns a specific model by its ID */ @@ -1989,12 +2038,15 @@ export type Project = { * real or fake (e.g., with a foo/bar model, it will be nested under foo even if such a model doesn't actually exist) */ modelsTree: ModelsTreeItemCollection; + /** Returns information about the potential effects of moving a project to a given workspace. */ + moveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRun; name: Scalars['String']['output']; object?: Maybe; /** Pending project access requests */ pendingAccessRequests?: Maybe>; /** Returns a list models that are being created from a file import */ pendingImportedModels: Array; + permissions: ProjectPermissionChecks; /** Active user's role for this project. `null` if request is not authenticated, or the project is not explicitly shared with you. */ role?: Maybe; /** Source apps used in any models of this project */ @@ -2007,7 +2059,7 @@ export type Project = { versions: VersionCollection; /** Return metadata about resources being requested in the viewer */ viewerResources: Array; - visibility: ProjectVisibility; + visibility: SimpleProjectVisibility; webhooks: WebhookCollection; workspace?: Maybe; workspaceId?: Maybe; @@ -2050,6 +2102,13 @@ export type ProjectCommentThreadsArgs = { }; +export type ProjectInvitableCollaboratorsArgs = { + cursor?: InputMaybe; + filter?: InputMaybe; + limit?: Scalars['Int']['input']; +}; + + export type ProjectModelArgs = { id: Scalars['String']['input']; }; @@ -2079,6 +2138,11 @@ export type ProjectModelsTreeArgs = { }; +export type ProjectMoveToWorkspaceDryRunArgs = { + workspaceId: Scalars['String']['input']; +}; + + export type ProjectObjectArgs = { id: Scalars['String']['input']; }; @@ -2153,6 +2217,7 @@ export type ProjectAutomationMutations = { createRevision: AutomationRevision; createTestAutomation: Automation; createTestAutomationRun: TestAutomationRun; + delete: Scalars['Boolean']['output']; /** * Trigger an automation with a fake "version created" trigger. The "version created" will * just refer to the last version of the model. @@ -2182,6 +2247,11 @@ export type ProjectAutomationMutationsCreateTestAutomationRunArgs = { }; +export type ProjectAutomationMutationsDeleteArgs = { + automationId: Scalars['ID']['input']; +}; + + export type ProjectAutomationMutationsTriggerArgs = { automationId: Scalars['ID']['input']; }; @@ -2223,7 +2293,11 @@ export type ProjectCollaborator = { __typename?: 'ProjectCollaborator'; id: Scalars['ID']['output']; role: Scalars['String']['output']; + /** The collaborator's workspace seat type for the workspace this project is in */ + seatType?: Maybe; user: LimitedUser; + /** The collaborator's workspace role for the workspace this project is in, if any */ + workspaceRole?: Maybe; }; export type ProjectCollection = { @@ -2393,6 +2467,17 @@ export enum ProjectModelsUpdatedMessageType { Updated = 'UPDATED' } +export type ProjectMoveToWorkspaceDryRun = { + __typename?: 'ProjectMoveToWorkspaceDryRun'; + addedToWorkspace: Array; + addedToWorkspaceTotalCount: Scalars['Int']['output']; +}; + + +export type ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs = { + limit?: InputMaybe; +}; + export type ProjectMutations = { __typename?: 'ProjectMutations'; /** Access request related mutations */ @@ -2480,6 +2565,28 @@ export enum ProjectPendingVersionsUpdatedMessageType { Updated = 'UPDATED' } +export type ProjectPermissionChecks = { + __typename?: 'ProjectPermissionChecks'; + canBroadcastActivity: PermissionCheckResult; + canCreateAutomation: PermissionCheckResult; + canCreateComment: PermissionCheckResult; + canCreateModel: PermissionCheckResult; + canDelete: PermissionCheckResult; + canLeave: PermissionCheckResult; + canMoveToWorkspace: PermissionCheckResult; + canRead: PermissionCheckResult; + canReadSettings: PermissionCheckResult; + canReadWebhooks: PermissionCheckResult; + canRequestRender: PermissionCheckResult; + canUpdate: PermissionCheckResult; + canUpdateAllowPublicComments: PermissionCheckResult; +}; + + +export type ProjectPermissionChecksCanMoveToWorkspaceArgs = { + workspaceId?: InputMaybe; +}; + export type ProjectRole = { __typename?: 'ProjectRole'; project: Project; @@ -2891,6 +2998,11 @@ export type Role = { resourceTarget: Scalars['String']['output']; }; +export type RootPermissionChecks = { + __typename?: 'RootPermissionChecks'; + canCreatePersonalProject: PermissionCheckResult; +}; + /** Available scopes. */ export type Scope = { __typename?: 'Scope'; @@ -3088,7 +3200,7 @@ export type ServerStats = { export type ServerWorkspacesInfo = { __typename?: 'ServerWorkspacesInfo'; /** Up-to-date prices for paid & non-invoiced Workspace plans */ - planPrices: Array; + planPrices?: Maybe; /** * This is a backend control variable for the workspaces feature set. * Since workspaces need a backend logic to be enabled, this is not enough as a feature flag. @@ -3105,6 +3217,12 @@ export type SetPrimaryUserEmailInput = { id: Scalars['ID']['input']; }; +/** Visibility without the "discoverable" option */ +export enum SimpleProjectVisibility { + Private = 'PRIVATE', + Unlisted = 'UNLISTED' +} + export type SmartTextEditorValue = { __typename?: 'SmartTextEditorValue'; /** File attachments, if any */ @@ -3748,8 +3866,10 @@ export type User = { isOnboardingFinished?: Maybe; /** Returns `true` if last visited project was "legacy" "personal project" outside of a workspace */ isProjectsActive?: Maybe; + meta: UserMeta; name: Scalars['String']['output']; notificationPreferences: Scalars['JSONObject']['output']; + permissions: RootPermissionChecks; profiles?: Maybe; /** Get pending project access request, that the user made */ projectAccessRequest?: Maybe; @@ -3851,6 +3971,7 @@ export type UserProjectsArgs = { cursor?: InputMaybe; filter?: InputMaybe; limit?: Scalars['Int']['input']; + sortBy?: InputMaybe>; }; @@ -3967,6 +4088,28 @@ export type UserGendoAiCredits = { used: Scalars['Int']['output']; }; +export type UserMeta = { + __typename?: 'UserMeta'; + legacyProjectsExplainerCollapsed: Scalars['Boolean']['output']; + newWorkspaceExplainerDismissed: Scalars['Boolean']['output']; +}; + +export type UserMetaMutations = { + __typename?: 'UserMetaMutations'; + setLegacyProjectsExplainerCollapsed: Scalars['Boolean']['output']; + setNewWorkspaceExplainerDismissed: Scalars['Boolean']['output']; +}; + + +export type UserMetaMutationsSetLegacyProjectsExplainerCollapsedArgs = { + value: Scalars['Boolean']['input']; +}; + + +export type UserMetaMutationsSetNewWorkspaceExplainerDismissedArgs = { + value: Scalars['Boolean']['input']; +}; + export type UserProjectCollection = { __typename?: 'UserProjectCollection'; cursor?: Maybe; @@ -3976,10 +4119,18 @@ export type UserProjectCollection = { }; export type UserProjectsFilter = { + /** + * If set to true, will also include streams that the user may not have an explicit role on, + * but has implicit access to because of workspaces + */ + includeImplicitAccess?: InputMaybe; /** Only include projects where user has the specified roles */ onlyWithRoles?: InputMaybe>; + /** Only include personal projects (not in any workspace) */ + personalOnly?: InputMaybe; /** Filter out projects by name */ search?: InputMaybe; + /** Only include projects in the specified workspace */ workspaceId?: InputMaybe; }; @@ -4058,8 +4209,9 @@ export type Version = { message?: Maybe; model: Model; parents?: Maybe>>; + permissions: VersionPermissionChecks; previewUrl: Scalars['String']['output']; - referencedObject: Scalars['String']['output']; + referencedObject?: Maybe; sourceApplication?: Maybe; totalChildrenCount?: Maybe; }; @@ -4135,6 +4287,12 @@ export type VersionMutationsUpdateArgs = { input: UpdateVersionInput; }; +export type VersionPermissionChecks = { + __typename?: 'VersionPermissionChecks'; + canReceive: PermissionCheckResult; + canUpdate: PermissionCheckResult; +}; + export type ViewerResourceGroup = { __typename?: 'ViewerResourceGroup'; /** Resource identifier used to refer to a collection of resource items */ @@ -4266,7 +4424,10 @@ export type Workspace = { /** Info about the workspace creation state */ creationState?: Maybe; customerPortalUrl?: Maybe; - /** The default role workspace members will receive for workspace projects. */ + /** + * The default role workspace members will receive for workspace projects. + * @deprecated Always the reviewer role. Will be removed in the future. + */ defaultProjectRole: Scalars['String']['output']; /** * The default region where project data will be stored, if set. If undefined, defaults to main/default @@ -4287,17 +4448,24 @@ export type Workspace = { /** Logo image as base64-encoded string */ logo?: Maybe; name: Scalars['String']['output']; + permissions: WorkspacePermissionChecks; plan?: Maybe; + /** Shows the plan prices localized for the given workspace */ + planPrices?: Maybe; projects: ProjectCollection; /** A Workspace is marked as readOnly if its trial period is finished or a paid plan is subscribed but payment has failed */ readOnly: Scalars['Boolean']['output']; /** Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */ role?: Maybe; + /** Active user's seat type for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */ + seatType?: Maybe; + seats?: Maybe; slug: Scalars['String']['output']; /** Information about the workspace's SSO configuration and the current user's SSO session, if present */ sso?: Maybe; subscription?: Maybe; team: WorkspaceCollaboratorCollection; + teamByRole: WorkspaceTeamByRole; updatedAt: Scalars['DateTime']['output']; }; @@ -4365,8 +4533,11 @@ export type WorkspaceBillingMutationsUpgradePlanArgs = { export type WorkspaceCollaborator = { __typename?: 'WorkspaceCollaborator'; id: Scalars['ID']['output']; + /** Date that the user joined the workspace. */ + joinDate: Scalars['DateTime']['output']; projectRoles: Array; role: Scalars['String']['output']; + seatType?: Maybe; user: LimitedUser; }; @@ -4386,6 +4557,8 @@ export type WorkspaceCollection = { export type WorkspaceCreateInput = { description?: InputMaybe; + /** Add this domain to the workspace as a verified domain and enable domain discoverability */ + enableDomainDiscoverabilityForDomain?: InputMaybe; /** Logo image as base64-encoded string */ logo?: InputMaybe; name: Scalars['String']['input']; @@ -4555,6 +4728,7 @@ export type WorkspaceMutations = { update: Workspace; updateCreationState: Scalars['Boolean']['output']; updateRole: Workspace; + updateSeatType: Workspace; }; @@ -4623,47 +4797,73 @@ export type WorkspaceMutationsUpdateRoleArgs = { input: WorkspaceRoleUpdateInput; }; + +export type WorkspaceMutationsUpdateSeatTypeArgs = { + input: WorkspaceUpdateSeatTypeInput; +}; + +export type WorkspacePaidPlanPrices = { + __typename?: 'WorkspacePaidPlanPrices'; + pro: WorkspacePlanPrice; + proUnlimited: WorkspacePlanPrice; + team: WorkspacePlanPrice; + teamUnlimited: WorkspacePlanPrice; +}; + export enum WorkspacePaymentMethod { Billing = 'billing', Invoice = 'invoice', Unpaid = 'unpaid' } +export type WorkspacePermissionChecks = { + __typename?: 'WorkspacePermissionChecks'; + canCreateProject: PermissionCheckResult; + canMoveProjectToWorkspace: PermissionCheckResult; +}; + + +export type WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs = { + projectId?: InputMaybe; +}; + export type WorkspacePlan = { __typename?: 'WorkspacePlan'; createdAt: Scalars['DateTime']['output']; name: WorkspacePlans; paymentMethod: WorkspacePaymentMethod; status: WorkspacePlanStatuses; + usage: WorkspacePlanUsage; }; export type WorkspacePlanPrice = { __typename?: 'WorkspacePlanPrice'; - id: Scalars['String']['output']; - monthly?: Maybe; - yearly?: Maybe; + monthly: Price; + yearly: Price; }; export enum WorkspacePlanStatuses { CancelationScheduled = 'cancelationScheduled', Canceled = 'canceled', - Expired = 'expired', PaymentFailed = 'paymentFailed', - Trial = 'trial', Valid = 'valid' } +export type WorkspacePlanUsage = { + __typename?: 'WorkspacePlanUsage'; + modelCount: Scalars['Int']['output']; + projectCount: Scalars['Int']['output']; +}; + export enum WorkspacePlans { Academia = 'academia', - Business = 'business', - BusinessInvoiced = 'businessInvoiced', Free = 'free', - Plus = 'plus', - PlusInvoiced = 'plusInvoiced', Pro = 'pro', - Starter = 'starter', - StarterInvoiced = 'starterInvoiced', + ProUnlimited = 'proUnlimited', + ProUnlimitedInvoiced = 'proUnlimitedInvoiced', Team = 'team', + TeamUnlimited = 'teamUnlimited', + TeamUnlimitedInvoiced = 'teamUnlimitedInvoiced', Unlimited = 'unlimited' } @@ -4726,6 +4926,8 @@ export type WorkspaceProjectMutationsUpdateRoleArgs = { export type WorkspaceProjectsFilter = { /** Filter out projects by name */ search?: InputMaybe; + /** Only return workspace projects that the active user has an explicit project role in */ + withProjectRoleOnly?: InputMaybe; }; export type WorkspaceProjectsUpdatedMessage = { @@ -4755,6 +4957,11 @@ export enum WorkspaceRole { Member = 'MEMBER' } +export type WorkspaceRoleCollection = { + __typename?: 'WorkspaceRoleCollection'; + totalCount: Scalars['Int']['output']; +}; + export type WorkspaceRoleDeleteInput = { userId: Scalars['String']['input']; workspaceId: Scalars['String']['input']; @@ -4767,6 +4974,22 @@ export type WorkspaceRoleUpdateInput = { workspaceId: Scalars['String']['input']; }; +export type WorkspaceSeatCollection = { + __typename?: 'WorkspaceSeatCollection'; + totalCount: Scalars['Int']['output']; +}; + +export enum WorkspaceSeatType { + Editor = 'editor', + Viewer = 'viewer' +} + +export type WorkspaceSeatsByType = { + __typename?: 'WorkspaceSeatsByType'; + editors?: Maybe; + viewers?: Maybe; +}; + export type WorkspaceSso = { __typename?: 'WorkspaceSso'; /** If null, the workspace does not have SSO configured */ @@ -4792,15 +5015,31 @@ export type WorkspaceSubscription = { __typename?: 'WorkspaceSubscription'; billingInterval: BillingInterval; createdAt: Scalars['DateTime']['output']; + currency: Currency; currentBillingCycleEnd: Scalars['DateTime']['output']; seats: WorkspaceSubscriptionSeats; updatedAt: Scalars['DateTime']['output']; }; +export type WorkspaceSubscriptionSeatCount = { + __typename?: 'WorkspaceSubscriptionSeatCount'; + /** Total number of seats in use by workspace users */ + assigned: Scalars['Int']['output']; + /** Total number of seats purchased and available in the current subscription cycle */ + available: Scalars['Int']['output']; +}; + export type WorkspaceSubscriptionSeats = { __typename?: 'WorkspaceSubscriptionSeats'; - guest: Scalars['Int']['output']; - plan: Scalars['Int']['output']; + editors: WorkspaceSubscriptionSeatCount; + viewers: WorkspaceSubscriptionSeatCount; +}; + +export type WorkspaceTeamByRole = { + __typename?: 'WorkspaceTeamByRole'; + admins?: Maybe; + guests?: Maybe; + members?: Maybe; }; export type WorkspaceTeamFilter = { @@ -4808,10 +5047,10 @@ export type WorkspaceTeamFilter = { roles?: InputMaybe>; /** Search for team members by name or email */ search?: InputMaybe; + seatType?: InputMaybe; }; export type WorkspaceUpdateInput = { - defaultProjectRole?: InputMaybe; description?: InputMaybe; discoverabilityEnabled?: InputMaybe; domainBasedMembershipProtectionEnabled?: InputMaybe; @@ -4822,6 +5061,12 @@ export type WorkspaceUpdateInput = { slug?: InputMaybe; }; +export type WorkspaceUpdateSeatTypeInput = { + seatType: WorkspaceSeatType; + userId: Scalars['String']['input']; + workspaceId: Scalars['String']['input']; +}; + export type WorkspaceUpdatedMessage = { __typename?: 'WorkspaceUpdatedMessage'; /** Workspace ID */ diff --git a/packages/dui3/package.json b/packages/dui3/package.json index 0ce614d09..741f59ee7 100644 --- a/packages/dui3/package.json +++ b/packages/dui3/package.json @@ -43,11 +43,11 @@ "vue-tippy": "^6.2.0" }, "devDependencies": { - "@graphql-codegen/cli": "^5.0.2", + "@graphql-codegen/cli": "^5.0.5", "@graphql-codegen/client-preset": "^4.3.0", "@nuxt/eslint": "^0.3.13", "@nuxtjs/tailwindcss": "^6.7.0", - "@parcel/watcher": "^2.4.1", + "@parcel/watcher": "^2.5.1", "@types/apollo-upload-client": "^17.0.1", "@types/eslint": "^8.56.10", "@types/lodash-es": "^4.17.6", diff --git a/packages/fileimport-service/.env.example b/packages/fileimport-service/.env.example index b6a66fe86..84560c028 100644 --- a/packages/fileimport-service/.env.example +++ b/packages/fileimport-service/.env.example @@ -4,4 +4,5 @@ POSTGRES_MAX_CONNECTIONS_FILE_IMPORT_SERVICE='1' POSTGRES_CONNECTION_ACQUIRE_TIMEOUT_MILLIS='16000' POSTGRES_CONNECTION_CREATE_TIMEOUT_MILLIS='5000' FF_WORKSPACES_MULTI_REGION_ENABLED=false -# IFC_DOTNET_DLL_PATH='packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll' +USE_LEGACY_IFC_IMPORTER=true +# IFC_DOTNET_DLL_PATH='packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll' \ No newline at end of file diff --git a/packages/fileimport-service/Dockerfile b/packages/fileimport-service/Dockerfile index 01964e30c..34bdec8cc 100644 --- a/packages/fileimport-service/Dockerfile +++ b/packages/fileimport-service/Dockerfile @@ -44,7 +44,7 @@ COPY packages/frontend-2/type-augmentations/stubs packages/frontend-2/type-augme COPY packages/shared/package.json packages/shared/ COPY packages/fileimport-service/package.json packages/fileimport-service/ -RUN yarn workspaces focus --all +RUN PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn workspaces focus --all # build shared libraries COPY packages/shared packages/shared/ diff --git a/packages/fileimport-service/README.md b/packages/fileimport-service/README.md index 2efde3e9a..09c9b38d5 100644 --- a/packages/fileimport-service/README.md +++ b/packages/fileimport-service/README.md @@ -11,3 +11,35 @@ The File Import service can parse either STL, OBJ, or IFC files using external p The parsers are responsible for extracting the necessary data from the files and storing it in the database. They are also responsible for creating a new Speckle model if necessary. The service is then responsible for updating the status of the `file_uploads` table, and for posting a Postgres notification. + +## Dev setup + +### Building/Running the .NET importer + +Requirements: + +- Ubuntu 24+ + +Do this on Ubuntu/OSX to install dotnet: + +```bash +# Add microsoft package repo +sudo apt update && sudo apt install -y wget apt-transport-https +wget https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb -O packages-microsoft-prod.deb +sudo dpkg -i packages-microsoft-prod.deb + +# Install dotnet sdk 8 +sudo apt update +sudo apt install -y dotnet-sdk-8.0 + +# Verify version +dotnet --version +``` + +Do this to build: + +```bash +cd ./packages/fileimport-service/src/ifc-dotnet + +dotnet publish ifc-converter.csproj -c Release -o output/ +``` diff --git a/packages/fileimport-service/package.json b/packages/fileimport-service/package.json index c742b4d5a..fb266c9e8 100644 --- a/packages/fileimport-service/package.json +++ b/packages/fileimport-service/package.json @@ -33,6 +33,7 @@ "dependencies": { "@speckle/shared": "workspace:^", "bcrypt": "^5.0.0", + "bull": "^4.16.5", "crypto": "^1.0.1", "crypto-random-string": "^3.2.0", "dotenv": "^16.4.5", @@ -47,7 +48,8 @@ "tarn": "^3.0.2", "undici": "^5.28.4", "valid-filename": "^3.1.0", - "web-ifc": "^0.0.36" + "web-ifc": "^0.0.36", + "znv": "^0.5.0" }, "devDependencies": { "@types/bcrypt": "^5.0.0", diff --git a/packages/fileimport-service/src/bin.ts b/packages/fileimport-service/src/bin.ts index 5a087f0b8..9af469c65 100644 --- a/packages/fileimport-service/src/bin.ts +++ b/packages/fileimport-service/src/bin.ts @@ -1,9 +1,15 @@ import '@/bootstrap.js' // This has side-effects and has to be imported first +import * as Environment from '@speckle/shared/environment' -import { main } from '@/controller/daemon.js' +const { FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = Environment.getFeatureFlags() +import { main as oldMain } from '@/controller/daemon.js' const start = () => { - void main() + if (FF_NEXT_GEN_FILE_IMPORTER_ENABLED) { + throw new Error('Not yet implemented') + } else { + void oldMain() + } } start() diff --git a/packages/fileimport-service/src/knex.ts b/packages/fileimport-service/src/clients/knex.ts similarity index 91% rename from packages/fileimport-service/src/knex.ts rename to packages/fileimport-service/src/clients/knex.ts index 6062b4761..d8311e30f 100644 --- a/packages/fileimport-service/src/knex.ts +++ b/packages/fileimport-service/src/clients/knex.ts @@ -1,10 +1,10 @@ -import Environment from '@speckle/shared/dist/commonjs/environment/index.js' +import * as Environment from '@speckle/shared/environment' import { loadMultiRegionsConfig, configureKnexClient -} from '@speckle/shared/dist/commonjs/environment/multiRegionConfig.js' +} from '@speckle/shared/environment/multiRegionConfig' import { logger } from '@/observability/logging.js' -import { Knex } from 'knex' +import type { Knex } from 'knex' const { FF_WORKSPACES_MULTI_REGION_ENABLED } = Environment.getFeatureFlags() diff --git a/packages/fileimport-service/src/controller/api.ts b/packages/fileimport-service/src/controller/api.ts index b7523df0c..7e8309320 100644 --- a/packages/fileimport-service/src/controller/api.ts +++ b/packages/fileimport-service/src/controller/api.ts @@ -3,7 +3,7 @@ import crs from 'crypto-random-string' import bcrypt from 'bcrypt' import { chunk } from 'lodash-es' import { logger as parentLogger } from '@/observability/logging.js' -import Observability from '@speckle/shared/dist/commonjs/observability/index.js' +import * as Observability from '@speckle/shared/observability' import type { Knex } from 'knex' import type { Logger } from 'pino' diff --git a/packages/fileimport-service/src/controller/daemon.ts b/packages/fileimport-service/src/controller/daemon.ts index 53cd84fe8..a1002ff7c 100644 --- a/packages/fileimport-service/src/controller/daemon.ts +++ b/packages/fileimport-service/src/controller/daemon.ts @@ -1,11 +1,10 @@ -import Environment from '@speckle/shared/dist/commonjs/environment/index.js' import { initPrometheusMetrics, metricDuration, metricInputFileSize, metricOperationErrors } from '@/controller/prometheusMetrics.js' -import { getDbClients } from '@/knex.js' +import { getDbClients } from '@/clients/knex.js' import { downloadFile } from '@/controller/filesApi.js' import fs from 'fs' @@ -14,11 +13,10 @@ import { spawn } from 'child_process' import { ServerAPI } from '@/controller/api.js' import { downloadDependencies } from '@/controller/objDependencies.js' import { logger } from '@/observability/logging.js' -import { Nullable, Scopes, wait } from '@speckle/shared' +import { Nullable, Scopes, wait, TIME_MS } from '@speckle/shared' import { Knex } from 'knex' import { Logger } from 'pino' - -const { FF_FILEIMPORT_IFC_DOTNET_ENABLED } = Environment.getFeatureFlags() +import { getIfcDllPath, useLegacyIfcImporter } from '@/controller/helpers/env.js' const HEALTHCHECK_FILE_PATH = '/tmp/last_successful_query' @@ -28,10 +26,10 @@ const TMP_RESULTS_PATH = '/tmp/import_result.json' let shouldExit = false -let TIME_LIMIT = 10 * 60 * 1000 +let TIME_LIMIT = 10 * TIME_MS.minute const providedTimeLimit = parseInt(process.env['FILE_IMPORT_TIME_LIMIT_MIN'] || '10') -if (providedTimeLimit) TIME_LIMIT = providedTimeLimit * 60 * 1000 +if (providedTimeLimit) TIME_LIMIT = providedTimeLimit * TIME_MS.minute async function startTask(knex: Knex) { const { rows } = (await knex.raw(` @@ -136,7 +134,7 @@ async function doTask( userId: info.userId, name: 'temp upload token', scopes: [Scopes.Streams.Write, Scopes.Streams.Read, Scopes.Profile.Read], - lifespan: 1000000 + lifespan: 1_000_000 }) tempUserToken = token @@ -156,27 +154,10 @@ async function doTask( taskLogger.info('Triggering importer for {fileType}') if (info.fileType.toLowerCase() === 'ifc') { - if (FF_FILEIMPORT_IFC_DOTNET_ENABLED) { - await runProcessWithTimeout( - taskLogger, - process.env['DOTNET_BINARY_PATH'] || 'dotnet', - [ - process.env['IFC_DOTNET_DLL_PATH'] || - '/speckle-server/packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll', - TMP_FILE_PATH, - TMP_RESULTS_PATH, - info.streamId, - `File upload: ${info.fileName}`, - existingBranch?.id || '', - info.branchName, - regionName - ], - { - USER_TOKEN: tempUserToken - }, - TIME_LIMIT - ) - } else { + if ( + info.fileName.toLowerCase().endsWith('.legacyimporter.ifc') || + useLegacyIfcImporter() + ) { await runProcessWithTimeout( taskLogger, process.env['NODE_BINARY_PATH'] || 'node', @@ -199,6 +180,25 @@ async function doTask( }, TIME_LIMIT ) + } else { + await runProcessWithTimeout( + taskLogger, + process.env['DOTNET_BINARY_PATH'] || 'dotnet', + [ + getIfcDllPath(), + TMP_FILE_PATH, + TMP_RESULTS_PATH, + info.streamId, + `File upload: ${info.fileName}`, + existingBranch?.id || '', + info.branchName, + regionName + ], + { + USER_TOKEN: tempUserToken + }, + TIME_LIMIT + ) } } else if (info.fileType.toLowerCase() === 'stl') { await runProcessWithTimeout( @@ -439,7 +439,7 @@ const doStuff = async () => { const task = await startTask(taskDb) fs.writeFile(HEALTHCHECK_FILE_PATH, '' + Date.now(), () => {}) if (!task) { - await wait(1000) + await wait(1 * TIME_MS.second) continue } await doTask(mainDb, regionName, taskDb, task) diff --git a/packages/fileimport-service/src/controller/filesApi.ts b/packages/fileimport-service/src/controller/filesApi.ts index 02afaa469..a0944826b 100644 --- a/packages/fileimport-service/src/controller/filesApi.ts +++ b/packages/fileimport-service/src/controller/filesApi.ts @@ -1,4 +1,4 @@ -import { ensureError } from '@speckle/shared/dist/esm/index.js' +import { ensureError } from '@speckle/shared' import fs from 'fs' import path from 'node:path' import { pipeline } from 'node:stream/promises' diff --git a/packages/fileimport-service/src/controller/helpers/env.ts b/packages/fileimport-service/src/controller/helpers/env.ts new file mode 100644 index 000000000..ba010df8d --- /dev/null +++ b/packages/fileimport-service/src/controller/helpers/env.ts @@ -0,0 +1,61 @@ +import path from 'node:path' +import url from 'node:url' +import file from 'node:fs' + +export const isDevEnv = () => { + return process.env.NODE_ENV === 'development' +} + +export const isTestEnv = () => { + return process.env.NODE_ENV === 'test' +} + +export const isDevOrTestEnv = () => isDevEnv() || isTestEnv() + +export const useLegacyIfcImporter = () => { + return ['true', '1'].includes(process.env.USE_LEGACY_IFC_IMPORTER || 'false') +} + +export const getPackageRootDirPath = () => { + const __filename = url.fileURLToPath(import.meta.url) + const __dirname = path.dirname(__filename) + + let root = path.resolve(__dirname, '../../../') + if (root.endsWith('dist')) { + // Resolved path may differ depending on whether running from dist or src (w/ ts-node) + root = path.resolve(root, '../') + } + + return root +} + +let cachedIfcDllPath: string | undefined = undefined +export const getIfcDllPath = () => { + if (cachedIfcDllPath) return cachedIfcDllPath + + const absolutePath = process.env['IFC_DOTNET_DLL_PATH'] + if (absolutePath && file.existsSync(absolutePath)) { + cachedIfcDllPath = absolutePath + return absolutePath + } + + if (isDevOrTestEnv()) { + const possiblePath = path.resolve( + getPackageRootDirPath(), + './src/ifc-dotnet/output/ifc-converter.dll' + ) + if (file.existsSync(possiblePath)) { + cachedIfcDllPath = absolutePath + return possiblePath + } + } + + const fallback = + '/speckle-server/packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll' + if (file.existsSync(fallback)) { + cachedIfcDllPath = fallback + return fallback + } + + throw new Error('Could not resolve .NET IFC DLL') +} diff --git a/packages/fileimport-service/src/controller/prometheusMetrics.ts b/packages/fileimport-service/src/controller/prometheusMetrics.ts index 172e59c52..9f56d6fcd 100644 --- a/packages/fileimport-service/src/controller/prometheusMetrics.ts +++ b/packages/fileimport-service/src/controller/prometheusMetrics.ts @@ -1,10 +1,11 @@ import http from 'http' import prometheusClient, { Counter, Summary } from 'prom-client' -import { getDbClients } from '@/knex.js' +import { getDbClients } from '@/clients/knex.js' import { Knex } from 'knex' import { Pool } from 'tarn' import { isObject } from 'lodash-es' import { IncomingMessage } from 'http' +import { TIME_MS } from '@speckle/shared' let metricQueryDuration: Summary | null = null let metricQueryErrors: Counter | null = null @@ -100,7 +101,7 @@ const initDBPrometheusMetricsFactory = db.on('query-response', (_data, obj) => { if (!isObject(obj) || !('__knexQueryUid' in obj)) return const queryId = String(obj.__knexQueryUid) - const durationSec = (Date.now() - queryStartTime[queryId]) / 1000 + const durationSec = (Date.now() - queryStartTime[queryId]) / TIME_MS.second delete queryStartTime[queryId] if (metricQueryDuration && !isNaN(durationSec)) metricQueryDuration.observe(durationSec) @@ -109,7 +110,7 @@ const initDBPrometheusMetricsFactory = db.on('query-error', (_err, querySpec) => { if (!isObject(querySpec) || !('__knexQueryUid' in querySpec)) return const queryId = String(querySpec.__knexQueryUid) - const durationSec = (Date.now() - queryStartTime[queryId]) / 1000 + const durationSec = (Date.now() - queryStartTime[queryId]) / TIME_MS.second delete queryStartTime[queryId] if (metricQueryDuration && !isNaN(durationSec)) diff --git a/packages/fileimport-service/src/ifc-dotnet/ifc-converter.csproj b/packages/fileimport-service/src/ifc-dotnet/ifc-converter.csproj index 42a5aad86..4c2bb63f3 100644 --- a/packages/fileimport-service/src/ifc-dotnet/ifc-converter.csproj +++ b/packages/fileimport-service/src/ifc-dotnet/ifc-converter.csproj @@ -10,7 +10,7 @@ - + diff --git a/packages/fileimport-service/src/ifc/import_file.js b/packages/fileimport-service/src/ifc/import_file.js index a57845eb4..27120fc67 100644 --- a/packages/fileimport-service/src/ifc/import_file.js +++ b/packages/fileimport-service/src/ifc/import_file.js @@ -1,7 +1,7 @@ import fs from 'fs' -import Observability from '@speckle/shared/dist/commonjs/observability/index.js' +import * as Observability from '@speckle/shared/observability' import { logger as parentLogger } from '@/observability/logging.js' -import { getDbClients } from '@/knex.js' +import { getDbClients } from '@/clients/knex.js' import { parseAndCreateCommitFactory } from '@/ifc/index.js' async function main() { diff --git a/packages/fileimport-service/src/ifc/index.js b/packages/fileimport-service/src/ifc/index.js index eea1e94c4..353821ed5 100644 --- a/packages/fileimport-service/src/ifc/index.js +++ b/packages/fileimport-service/src/ifc/index.js @@ -1,6 +1,6 @@ import { performance } from 'perf_hooks' import { fetch } from 'undici' -import Observability from '@speckle/shared/dist/commonjs/observability/index.js' +import * as Observability from '@speckle/shared/observability' import { ServerAPI } from '@/controller/api.js' import { logger as parentLogger } from '@/observability/logging.js' import { IFCParser } from '@/ifc/parser.js' diff --git a/packages/fileimport-service/src/ifc/parser.js b/packages/fileimport-service/src/ifc/parser.js index e960fde30..e0dddafbb 100644 --- a/packages/fileimport-service/src/ifc/parser.js +++ b/packages/fileimport-service/src/ifc/parser.js @@ -1,6 +1,6 @@ import { performance } from 'perf_hooks' import WebIFC from 'web-ifc/web-ifc-api-node.js' -import Observability from '@speckle/shared/dist/commonjs/observability/index.js' +import * as Observability from '@speckle/shared/observability' import { logger as parentLogger } from '@/observability/logging.js' import { getHash, diff --git a/packages/fileimport-service/src/observability/logging.ts b/packages/fileimport-service/src/observability/logging.ts index 6ef8bca14..0b5a53c1b 100644 --- a/packages/fileimport-service/src/observability/logging.ts +++ b/packages/fileimport-service/src/observability/logging.ts @@ -1,10 +1,10 @@ -import Observability from '@speckle/shared/dist/commonjs/observability/index.js' +import * as Observability from '@speckle/shared/observability' // loggers for specific components within normal operation export const logger = Observability.extendLoggerComponent( Observability.getLogger( process.env.LOG_LEVEL || 'info', - process.env.LOG_PRETTY === 'true' + process.env.LOG_PRETTY === 'true' && !process.env.FORCE_NO_PRETTY ), 'fileimport-service' ) diff --git a/packages/frontend-2/Dockerfile b/packages/frontend-2/Dockerfile index d4777a56b..cda52f5be 100644 --- a/packages/frontend-2/Dockerfile +++ b/packages/frontend-2/Dockerfile @@ -1,6 +1,5 @@ FROM node:22-bookworm-slim@sha256:221ee67425de7a3c11ce4e81e63e50caaec82ede3a7d34599ab20e59d29a0cb5 AS build-stage ARG NODE_ENV=production -ARG SPECKLE_SERVER_VERSION=custom ARG BUILD_SOURCEMAPS=false # for better sourcemaps (the app still gets minified at the end) @@ -20,7 +19,7 @@ COPY packages/ui-components/package.json ./packages/ui-components/ COPY packages/ui-components-nuxt/package.json ./packages/ui-components-nuxt/ COPY packages/tailwind-theme/package.json ./packages/tailwind-theme/ COPY packages/frontend-2/package.json ./packages/frontend-2/ -COPY packages/frontend-2/type-augmentations ./packages/frontend-2/ +COPY packages/frontend-2/type-augmentations ./packages/frontend-2/ COPY packages/objectloader ./packages/objectloader/ COPY packages/viewer ./packages/viewer/ @@ -31,9 +30,15 @@ COPY packages/tailwind-theme ./packages/tailwind-theme/ COPY packages/frontend-2 ./packages/frontend-2/ # hadolint ignore=DL3059 -RUN yarn workspaces focus -A +RUN PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn workspaces focus -A # hadolint ignore=DL3059 -RUN yarn workspaces foreach -W run build +RUN yarn workspaces foreach --exclude "@speckle/frontend-2" -W run build + +# improve caching as other builds do not depend on server version +ARG SPECKLE_SERVER_VERSION=custom +# hadolint ignore=DL3059 +RUN yarn workspaces foreach --include "packages/frontend-2" -W run build + ENV TINI_VERSION=v0.19.0 RUN apt-get update -y \ diff --git a/packages/frontend-2/assets/images/workspaces-dark.png b/packages/frontend-2/assets/images/workspaces-dark.png deleted file mode 100644 index 9cbfd1bee..000000000 Binary files a/packages/frontend-2/assets/images/workspaces-dark.png and /dev/null differ diff --git a/packages/frontend-2/assets/images/workspaces.png b/packages/frontend-2/assets/images/workspaces.png deleted file mode 100644 index ff17fa397..000000000 Binary files a/packages/frontend-2/assets/images/workspaces.png and /dev/null differ diff --git a/packages/frontend-2/components/Cal/PopUp.vue b/packages/frontend-2/components/Cal/PopUp.vue new file mode 100644 index 000000000..8f4866c70 --- /dev/null +++ b/packages/frontend-2/components/Cal/PopUp.vue @@ -0,0 +1,41 @@ + + + diff --git a/packages/frontend-2/components/Cal/Widget.vue b/packages/frontend-2/components/Cal/Widget.vue new file mode 100644 index 000000000..448bfcec3 --- /dev/null +++ b/packages/frontend-2/components/Cal/Widget.vue @@ -0,0 +1,24 @@ + + + diff --git a/packages/frontend-2/components/automate/automation/CreateDialog.vue b/packages/frontend-2/components/automate/automation/CreateDialog.vue index 691ebacce..63b482964 100644 --- a/packages/frontend-2/components/automate/automation/CreateDialog.vue +++ b/packages/frontend-2/components/automate/automation/CreateDialog.vue @@ -54,23 +54,15 @@ v-model:has-errors="hasParameterErrors" :fn="selectedFunction" /> - + @@ -190,8 +182,7 @@ const shouldShowStepsWidget = computed(() => { }) const enableSubmitTestAutomation = computed(() => { - const isValidInput = - !!automationName.value && !!selectedModel.value && !!selectedFunction.value + const isValidInput = !!automationName.value && !!selectedModel.value const isLoading = creationLoading.value return isValidInput && !isLoading @@ -352,7 +343,7 @@ const onDetailsSubmit = handleDetailsSubmit(async () => { const parameters = functionParameters.value const name = automationName.value - if (!fn || !project || !model || !name?.length || !fnRelease) { + if (!project || !model || !name?.length) { logger.error('Missing required data', { fn, project, @@ -375,7 +366,6 @@ const onDetailsSubmit = handleDetailsSubmit(async () => { projectId: project.id, input: { name, - functionId: fn.id, modelId: model.id } }) @@ -390,6 +380,19 @@ const onDetailsSubmit = handleDetailsSubmit(async () => { return } + // Test automations can be created without functions + if (!fn || !fnRelease) { + logger.error('Missing required data', { + fn, + project, + model, + parameters, + name, + fnRelease + }) + return + } + const createRes = await createAutomation({ projectId: project.id, input: { diff --git a/packages/frontend-2/components/automate/automation/create-dialog/SelectFunctionStep.vue b/packages/frontend-2/components/automate/automation/create-dialog/SelectFunctionStep.vue index b94d3210c..9ba04ff0c 100644 --- a/packages/frontend-2/components/automate/automation/create-dialog/SelectFunctionStep.vue +++ b/packages/frontend-2/components/automate/automation/create-dialog/SelectFunctionStep.vue @@ -45,11 +45,11 @@ import { usePaginatedQuery } from '~/lib/common/composables/graphql' const searchQuery = graphql(` query AutomationCreateDialogFunctionsSearch( $workspaceId: String! - $search: String + $filter: AutomateFunctionsFilter $cursor: String = null ) { workspace(id: $workspaceId) { - automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) { + automateFunctions(limit: 20, cursor: $cursor, filter: $filter) { cursor totalCount items { @@ -68,10 +68,12 @@ const props = withDefaults( pageSize?: Optional showLabel?: Optional showRequired?: Optional + isTestAutomation?: Optional }>(), { showLabel: true, - showRequired: true + showRequired: true, + isTestAutomation: false } ) const selectedFunction = defineModel>( @@ -91,10 +93,14 @@ const { query: searchQuery, baseVariables: computed(() => ({ workspaceId: props.workspaceId ?? '', - search: search.value?.length ? search.value : '', - cursor: null as Nullable + cursor: null as Nullable, + filter: { + search: search.value?.length ? search.value : undefined, + includeFeatured: props.isTestAutomation ? false : true, + requireRelease: props.isTestAutomation ? false : true + } })), - resolveKey: (vars) => [vars.search || ''], + resolveKey: (vars) => [vars.filter.search || ''], resolveCurrentResult: (res) => res?.workspace?.automateFunctions, resolveNextPageVariables: (baseVars, cursor) => ({ ...baseVars, diff --git a/packages/frontend-2/components/automate/functions/page/Items.vue b/packages/frontend-2/components/automate/functions/page/Items.vue index ebef1568e..d64940749 100644 --- a/packages/frontend-2/components/automate/functions/page/Items.vue +++ b/packages/frontend-2/components/automate/functions/page/Items.vue @@ -17,7 +17,6 @@ diff --git a/packages/frontend-2/components/billing/UsageAlert.vue b/packages/frontend-2/components/billing/UsageAlert.vue new file mode 100644 index 000000000..bd6ab5b6f --- /dev/null +++ b/packages/frontend-2/components/billing/UsageAlert.vue @@ -0,0 +1,27 @@ + + + diff --git a/packages/frontend-2/components/common/TitleDescription.vue b/packages/frontend-2/components/common/TitleDescription.vue index c71c390ff..5fc4424fa 100644 --- a/packages/frontend-2/components/common/TitleDescription.vue +++ b/packages/frontend-2/components/common/TitleDescription.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend-2/components/error/page/Renderer.vue b/packages/frontend-2/components/error/page/Renderer.vue index 1b00b7eaf..7f42de373 100644 --- a/packages/frontend-2/components/error/page/Renderer.vue +++ b/packages/frontend-2/components/error/page/Renderer.vue @@ -16,15 +16,16 @@ const props = defineProps<{ } }>() +const route = useRoute() + +const isProjectRoute = computed(() => route.path.match(/\/projects\/[^/]+/)) +const isWorkspaceRoute = computed(() => route.path.match(/\/workspaces\/[^/]+/)) + const finalError = computed(() => formatAppError(props.error)) const isNoProjectAccessError = computed( - () => - finalError.value.statusCode === 403 && - finalError.value.message.includes('You do not have access to this project') + () => finalError.value.statusCode === 403 && isProjectRoute.value ) const isNoWorkspaceAccessError = computed( - () => - finalError.value.statusCode === 403 && - finalError.value.message.includes('You do not have access to this workspace') + () => finalError.value.statusCode === 403 && isWorkspaceRoute.value ) diff --git a/packages/frontend-2/components/form/file-upload/ProgressRow.vue b/packages/frontend-2/components/form/file-upload/ProgressRow.vue index a1508ad98..ffcde8f8c 100644 --- a/packages/frontend-2/components/form/file-upload/ProgressRow.vue +++ b/packages/frontend-2/components/form/file-upload/ProgressRow.vue @@ -1,7 +1,5 @@ diff --git a/packages/frontend-2/components/header/WorkspaceSwitcher/List.vue b/packages/frontend-2/components/header/WorkspaceSwitcher/List.vue new file mode 100644 index 000000000..a97931fb7 --- /dev/null +++ b/packages/frontend-2/components/header/WorkspaceSwitcher/List.vue @@ -0,0 +1,44 @@ + + + diff --git a/packages/frontend-2/components/header/WorkspaceSwitcher/ListItem.vue b/packages/frontend-2/components/header/WorkspaceSwitcher/ListItem.vue new file mode 100644 index 000000000..922e70f99 --- /dev/null +++ b/packages/frontend-2/components/header/WorkspaceSwitcher/ListItem.vue @@ -0,0 +1,79 @@ + + + diff --git a/packages/frontend-2/components/header/WorkspaceSwitcher/WorkspaceSwitcher.vue b/packages/frontend-2/components/header/WorkspaceSwitcher/WorkspaceSwitcher.vue index 4858edfc1..c426bbfc0 100644 --- a/packages/frontend-2/components/header/WorkspaceSwitcher/WorkspaceSwitcher.vue +++ b/packages/frontend-2/components/header/WorkspaceSwitcher/WorkspaceSwitcher.vue @@ -7,13 +7,13 @@ diff --git a/packages/frontend-2/components/header/WorkspaceSwitcher/header/Header.vue b/packages/frontend-2/components/header/WorkspaceSwitcher/header/Header.vue index 2753dc81d..fb179ccd3 100644 --- a/packages/frontend-2/components/header/WorkspaceSwitcher/header/Header.vue +++ b/packages/frontend-2/components/header/WorkspaceSwitcher/header/Header.vue @@ -24,6 +24,7 @@ diff --git a/packages/frontend-2/components/header/WorkspaceSwitcher/header/Workspace.vue b/packages/frontend-2/components/header/WorkspaceSwitcher/header/Workspace.vue index aaa22d16f..604817e08 100644 --- a/packages/frontend-2/components/header/WorkspaceSwitcher/header/Workspace.vue +++ b/packages/frontend-2/components/header/WorkspaceSwitcher/header/Workspace.vue @@ -6,7 +6,8 @@ :to="workspaceRoute(activeWorkspaceSlug || '')" >

- {{ workspace?.plan?.name }} · {{ workspace?.team?.totalCount ?? 0 }} member{{ + {{ formatName(workspace?.plan?.name) }} · + {{ workspace?.team?.totalCount ?? 0 }} member{{ (workspace?.team?.totalCount ?? 0) > 1 ? 's' : '' }}

@@ -22,22 +23,27 @@ Settings - - +
- Invite members - + + Invite members + +
- - @@ -48,10 +54,10 @@ import type { HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragment } from '~ import { Roles, type MaybeNullOrUndefined } from '@speckle/shared' import { workspaceRoute, settingsWorkspaceRoutes } from '~/lib/common/helpers/route' import { useNavigation } from '~~/lib/navigation/composables/navigation' +import { formatName } from '~/lib/billing/helpers/plan' graphql(` fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace { - ...InviteDialogWorkspace_Workspace id name logo @@ -65,11 +71,13 @@ graphql(` } `) -defineProps<{ +defineEmits(['show-invite-dialog']) + +const props = defineProps<{ workspace: MaybeNullOrUndefined }>() const { activeWorkspaceSlug } = useNavigation() -const showInviteDialog = ref(false) +const isAdmin = computed(() => props.workspace?.role === Roles.Workspace.Admin) diff --git a/packages/frontend-2/components/header/NavBar.vue b/packages/frontend-2/components/header/nav/Bar.vue similarity index 89% rename from packages/frontend-2/components/header/NavBar.vue rename to packages/frontend-2/components/header/nav/Bar.vue index 6b08a5cb7..533507e4e 100644 --- a/packages/frontend-2/components/header/NavBar.vue +++ b/packages/frontend-2/components/header/nav/Bar.vue @@ -24,7 +24,8 @@ -
+ +
) const showWorkspaceSwitcher = computed( - () => - isWorkspacesEnabled.value && isWorkspaceNewPlansEnabled.value && activeUser.value + () => isWorkspacesEnabled.value && activeUser.value ) diff --git a/packages/frontend-2/components/header/NavLink.vue b/packages/frontend-2/components/header/nav/Link.vue similarity index 100% rename from packages/frontend-2/components/header/NavLink.vue rename to packages/frontend-2/components/header/nav/Link.vue diff --git a/packages/frontend-2/components/header/NavShare.vue b/packages/frontend-2/components/header/nav/Share.vue similarity index 100% rename from packages/frontend-2/components/header/NavShare.vue rename to packages/frontend-2/components/header/nav/Share.vue diff --git a/packages/frontend-2/components/header/NavUserMenu.vue b/packages/frontend-2/components/header/nav/UserMenu.vue similarity index 100% rename from packages/frontend-2/components/header/NavUserMenu.vue rename to packages/frontend-2/components/header/nav/UserMenu.vue diff --git a/packages/frontend-2/components/header/nav/notifications/Invite.vue b/packages/frontend-2/components/header/nav/notifications/Invite.vue new file mode 100644 index 000000000..38152e76f --- /dev/null +++ b/packages/frontend-2/components/header/nav/notifications/Invite.vue @@ -0,0 +1,71 @@ + + diff --git a/packages/frontend-2/components/header/nav/notifications/Notifications.vue b/packages/frontend-2/components/header/nav/notifications/Notifications.vue new file mode 100644 index 000000000..448e2afd3 --- /dev/null +++ b/packages/frontend-2/components/header/nav/notifications/Notifications.vue @@ -0,0 +1,82 @@ + + diff --git a/packages/frontend-2/components/header/nav/notifications/ProjectInvite.vue b/packages/frontend-2/components/header/nav/notifications/ProjectInvite.vue new file mode 100644 index 000000000..2bd45f68a --- /dev/null +++ b/packages/frontend-2/components/header/nav/notifications/ProjectInvite.vue @@ -0,0 +1,69 @@ + + + diff --git a/packages/frontend-2/components/header/nav/notifications/WorkspaceInvite.vue b/packages/frontend-2/components/header/nav/notifications/WorkspaceInvite.vue new file mode 100644 index 000000000..1326d3423 --- /dev/null +++ b/packages/frontend-2/components/header/nav/notifications/WorkspaceInvite.vue @@ -0,0 +1,63 @@ + + diff --git a/packages/frontend-2/components/invite/dialog/Workspace.vue b/packages/frontend-2/components/invite/dialog/Workspace.vue index 446d962f8..83a0f094e 100644 --- a/packages/frontend-2/components/invite/dialog/Workspace.vue +++ b/packages/frontend-2/components/invite/dialog/Workspace.vue @@ -1,59 +1,57 @@ + diff --git a/packages/frontend-2/components/invite/dialog/project/workspaceMembers/Row.vue b/packages/frontend-2/components/invite/dialog/project/workspaceMembers/Row.vue deleted file mode 100644 index 10fc294ed..000000000 --- a/packages/frontend-2/components/invite/dialog/project/workspaceMembers/Row.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - diff --git a/packages/frontend-2/components/invite/dialog/project/workspaceMembers/WorkspaceMembers.vue b/packages/frontend-2/components/invite/dialog/project/workspaceMembers/WorkspaceMembers.vue deleted file mode 100644 index 84befad50..000000000 --- a/packages/frontend-2/components/invite/dialog/project/workspaceMembers/WorkspaceMembers.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/packages/frontend-2/components/invite/dialog/shared/SelectUsers.vue b/packages/frontend-2/components/invite/dialog/shared/SelectUsers.vue index be3285d71..cf8b59adf 100644 --- a/packages/frontend-2/components/invite/dialog/shared/SelectUsers.vue +++ b/packages/frontend-2/components/invite/dialog/shared/SelectUsers.vue @@ -1,139 +1,87 @@ diff --git a/packages/frontend-2/components/invite/dialog/workspace/SelectRole.vue b/packages/frontend-2/components/invite/dialog/workspace/SelectRole.vue new file mode 100644 index 000000000..e80f518d7 --- /dev/null +++ b/packages/frontend-2/components/invite/dialog/workspace/SelectRole.vue @@ -0,0 +1,26 @@ + + + diff --git a/packages/frontend-2/components/onboarding/questions/Form.vue b/packages/frontend-2/components/onboarding/questions/Form.vue index f1080b1f8..56e3cbe9f 100644 --- a/packages/frontend-2/components/onboarding/questions/Form.vue +++ b/packages/frontend-2/components/onboarding/questions/Form.vue @@ -15,7 +15,7 @@ link color="subtle" full-width - @click="setUserOnboardingComplete()" + @click="onSkip" > Skip @@ -28,12 +28,16 @@ import { useForm } from 'vee-validate' import type { OnboardingRole, OnboardingPlan, OnboardingSource } from '@speckle/shared' import { useProcessOnboarding } from '~~/lib/auth/composables/onboarding' -import { homeRoute, workspaceJoinRoute } from '~/lib/common/helpers/route' +import { homeRoute, bookDemoRoute } from '~/lib/common/helpers/route' import { useDiscoverableWorkspaces } from '~/lib/workspaces/composables/discoverableWorkspaces' +import { useBreakpoints } from '@vueuse/core' +import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind' const isOnboardingForced = useIsOnboardingForced() const isWorkspacesEnabled = useIsWorkspacesEnabled() const { hasDiscoverableWorkspaces } = useDiscoverableWorkspaces() +const breakpoints = useBreakpoints(TailwindBreakpoints) +const isMobile = breakpoints.smaller('sm') const { setUserOnboardingComplete, setMixpanelSegments } = useProcessOnboarding() @@ -54,10 +58,15 @@ const onSubmit = handleSubmit(async () => { plans: values.plan, source: values.source }) - if (isWorkspacesEnabled.value && hasDiscoverableWorkspaces.value) { - navigateTo(workspaceJoinRoute) + if (isWorkspacesEnabled.value && hasDiscoverableWorkspaces.value && !isMobile.value) { + navigateTo(bookDemoRoute) } else { navigateTo(homeRoute) } }) + +const onSkip = () => { + setUserOnboardingComplete() + navigateTo(!isMobile.value && isWorkspacesEnabled.value ? bookDemoRoute : homeRoute) +} diff --git a/packages/frontend-2/components/onboarding/questions/PlanSelect.vue b/packages/frontend-2/components/onboarding/questions/PlanSelect.vue index 1de7f0b99..32af3c783 100644 --- a/packages/frontend-2/components/onboarding/questions/PlanSelect.vue +++ b/packages/frontend-2/components/onboarding/questions/PlanSelect.vue @@ -11,7 +11,7 @@ show-label allow-unset clearable - :items="plans" + :items="shuffledPlans" >