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 28676ce7a..bfde5a1b8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -789,6 +789,21 @@ jobs: command: yarn test:single-run working_directory: 'packages/shared' + - 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 resource_class: large 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/.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/README.md b/README.md index d7ba3e224..f64ba2cc1 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 18. diff --git a/packages/dui3/lib/common/generated/gql/graphql.ts b/packages/dui3/lib/common/generated/gql/graphql.ts index 6aa317754..b01619308 100644 --- a/packages/dui3/lib/common/generated/gql/graphql.ts +++ b/packages/dui3/lib/common/generated/gql/graphql.ts @@ -588,6 +588,7 @@ export type CheckoutSession = { export type CheckoutSessionInput = { billingInterval: BillingInterval; + currency?: InputMaybe; isCreateFlow?: InputMaybe; workspaceId: Scalars['ID']['input']; workspacePlan: PaidWorkspacePlans; @@ -609,8 +610,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 +622,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 +744,11 @@ export type CommentMutationsReplyArgs = { input: CreateCommentReplyInput; }; +export type CommentPermissionChecks = { + __typename?: 'CommentPermissionChecks'; + canArchive: PermissionCheckResult; +}; + export type CommentReplyAuthorCollection = { __typename?: 'CommentReplyAuthorCollection'; items: Array; @@ -926,6 +933,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']; @@ -1240,6 +1258,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; @@ -1298,6 +1317,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>; @@ -1879,8 +1905,10 @@ export enum PaidWorkspacePlans { Business = 'business', Plus = 'plus', Pro = 'pro', + ProUnlimited = 'proUnlimited', Starter = 'starter', - Team = 'team' + Team = 'team', + TeamUnlimited = 'teamUnlimited' } export type PasswordStrengthCheckFeedback = { @@ -2523,7 +2551,22 @@ export enum ProjectPendingVersionsUpdatedMessageType { export type ProjectPermissionChecks = { __typename?: 'ProjectPermissionChecks'; + canBroadcastActivity: PermissionCheckResult; + canCreateComment: PermissionCheckResult; + canCreateModel: 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 = { @@ -2937,6 +2980,11 @@ export type Role = { resourceTarget: Scalars['String']['output']; }; +export type RootPermissionChecks = { + __typename?: 'RootPermissionChecks'; + canCreatePersonalProject: PermissionCheckResult; +}; + /** Available scopes. */ export type Scope = { __typename?: 'Scope'; @@ -3134,7 +3182,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. @@ -3802,6 +3850,7 @@ export type User = { isProjectsActive?: Maybe; name: Scalars['String']['output']; notificationPreferences: Scalars['JSONObject']['output']; + permissions: RootPermissionChecks; profiles?: Maybe; /** Get pending project access request, that the user made */ projectAccessRequest?: Maybe; @@ -4113,8 +4162,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; }; @@ -4190,6 +4240,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 */ @@ -4347,6 +4403,8 @@ export type Workspace = { 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']; @@ -4696,6 +4754,14 @@ 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', @@ -4705,6 +4771,12 @@ export enum WorkspacePaymentMethod { export type WorkspacePermissionChecks = { __typename?: 'WorkspacePermissionChecks'; canCreateProject: PermissionCheckResult; + canMoveProjectToWorkspace: PermissionCheckResult; +}; + + +export type WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs = { + projectId?: InputMaybe; }; export type WorkspacePlan = { @@ -4718,9 +4790,8 @@ export type WorkspacePlan = { export type WorkspacePlanPrice = { __typename?: 'WorkspacePlanPrice'; - id: Scalars['String']['output']; - monthly?: Maybe; - yearly?: Maybe; + monthly: Price; + yearly: Price; }; export enum WorkspacePlanStatuses { @@ -4746,9 +4817,13 @@ export enum WorkspacePlans { Plus = 'plus', PlusInvoiced = 'plusInvoiced', Pro = 'pro', + ProUnlimited = 'proUnlimited', + ProUnlimitedInvoiced = 'proUnlimitedInvoiced', Starter = 'starter', StarterInvoiced = 'starterInvoiced', Team = 'team', + TeamUnlimited = 'teamUnlimited', + TeamUnlimitedInvoiced = 'teamUnlimitedInvoiced', Unlimited = 'unlimited' } @@ -4900,6 +4975,7 @@ export type WorkspaceSubscription = { __typename?: 'WorkspaceSubscription'; billingInterval: BillingInterval; createdAt: Scalars['DateTime']['output']; + currency: Currency; currentBillingCycleEnd: Scalars['DateTime']['output']; seats: WorkspaceSubscriptionSeats; updatedAt: Scalars['DateTime']['output']; diff --git a/packages/frontend-2/components/billing/Alert.vue b/packages/frontend-2/components/billing/Alert.vue index fb1e3f9ca..7085bf73d 100644 --- a/packages/frontend-2/components/billing/Alert.vue +++ b/packages/frontend-2/components/billing/Alert.vue @@ -1,15 +1,13 @@ @@ -21,10 +19,14 @@ import { } from '~/lib/common/generated/gql/graphql' import { useBillingActions } from '~/lib/billing/composables/actions' import type { AlertAction, AlertColor } from '@speckle/ui-components' +import { type MaybeNullOrUndefined, Roles } from '@speckle/shared' +import { settingsWorkspaceRoutes } from '~/lib/common/helpers/route' graphql(` fragment BillingAlert_Workspace on Workspace { id + role + slug plan { name status @@ -38,20 +40,14 @@ graphql(` `) const props = defineProps<{ - workspace: BillingAlert_WorkspaceFragment - actions?: Array - condensed?: boolean + workspace: MaybeNullOrUndefined + hideSettingsLinks?: boolean }>() const { billingPortalRedirect } = useBillingActions() -const planStatus = computed(() => props.workspace.plan?.status) -const isPaymentFailed = computed( - () => planStatus.value === WorkspacePlanStatuses.PaymentFailed -) -const isScheduledForCancelation = computed( - () => planStatus.value === WorkspacePlanStatuses.CancelationScheduled -) +const planStatus = computed(() => props.workspace?.plan?.status) + const title = computed(() => { switch (planStatus.value) { case WorkspacePlanStatuses.CancelationScheduled: @@ -64,24 +60,19 @@ const title = computed(() => { return '' } }) + const description = computed(() => { switch (planStatus.value) { case WorkspacePlanStatuses.CancelationScheduled: return 'Once the current billing cycle ends your workspace will enter read-only mode. Renew your subscription to undo.' case WorkspacePlanStatuses.Canceled: - return 'Your workspace has been cancelled and is in read-only mode. Subscribe to a plan to regain full access.' + return 'Your workspace has been cancelled and is in read-only mode. Resubscribe to a plan to regain full access.' case WorkspacePlanStatuses.PaymentFailed: return "Update your payment information now to ensure your workspace doesn't go into maintenance mode." default: return '' } }) -const showBillingAlert = computed( - () => - planStatus.value === WorkspacePlanStatuses.PaymentFailed || - planStatus.value === WorkspacePlanStatuses.Canceled || - planStatus.value === WorkspacePlanStatuses.CancelationScheduled -) const alertColor = computed(() => { switch (planStatus.value) { @@ -95,20 +86,32 @@ const alertColor = computed(() => { } }) -const actions = computed((): AlertAction[] => { - const actions: Array = props.actions ?? [] +const isWorkspaceGuest = computed(() => props.workspace?.role === Roles.Workspace.Guest) - if (isPaymentFailed.value) { +const actions = computed((): AlertAction[] => { + const actions: Array = [] + + if (isWorkspaceGuest.value) return actions + + if (planStatus.value === WorkspacePlanStatuses.PaymentFailed) { actions.push({ title: 'Update payment information', - onClick: () => billingPortalRedirect(props.workspace.id), - disabled: !props.workspace.id + onClick: () => billingPortalRedirect(props.workspace?.id) }) - } else if (isScheduledForCancelation.value) { + } + + if (planStatus.value === WorkspacePlanStatuses.CancelationScheduled) { actions.push({ title: 'Renew subscription', - onClick: () => billingPortalRedirect(props.workspace.id), - disabled: !props.workspace.id + onClick: () => billingPortalRedirect(props.workspace?.id) + }) + } + + if (planStatus.value === WorkspacePlanStatuses.Canceled && !props.hideSettingsLinks) { + actions.push({ + title: 'Resubscribe', + onClick: () => + navigateTo(settingsWorkspaceRoutes.billing.route(props.workspace?.slug || '')) }) } diff --git a/packages/frontend-2/components/billing/TransitionCards.vue b/packages/frontend-2/components/billing/TransitionCards.vue index 01197b935..a74116366 100644 --- a/packages/frontend-2/components/billing/TransitionCards.vue +++ b/packages/frontend-2/components/billing/TransitionCards.vue @@ -12,7 +12,7 @@ diff --git a/packages/frontend-2/components/dashboard/Sidebar.vue b/packages/frontend-2/components/dashboard/Sidebar.vue index d2cc714cf..500da5180 100644 --- a/packages/frontend-2/components/dashboard/Sidebar.vue +++ b/packages/frontend-2/components/dashboard/Sidebar.vue @@ -35,42 +35,10 @@ - - - - - - - - - +