diff --git a/packages/frontend-2/assets/images/viewer/saved-views/plan_upsell.webp b/packages/frontend-2/assets/images/viewer/saved-views/plan_upsell.webp deleted file mode 100644 index 73812efd2..000000000 Binary files a/packages/frontend-2/assets/images/viewer/saved-views/plan_upsell.webp and /dev/null differ diff --git a/packages/frontend-2/components/viewer/saved-views/Panel.vue b/packages/frontend-2/components/viewer/saved-views/Panel.vue index 0e407c8d1..ecff814c3 100644 --- a/packages/frontend-2/components/viewer/saved-views/Panel.vue +++ b/packages/frontend-2/components/viewer/saved-views/Panel.vue @@ -97,7 +97,6 @@ /> - -
- Saved Views -
-
Save custom views
-
-

Upgrade to a business plan to save, organise and present

-
    -
  • It's cool
  • -
  • It's nice
  • -
  • It's got enough spice
  • -
-
-
-
- Upgrade - Learn more -
-
- diff --git a/packages/server/modules/viewer/tests/integration/savedViewsCrud.graph.spec.ts b/packages/server/modules/viewer/tests/integration/savedViewsCrud.graph.spec.ts index 5482647cf..db7d11c23 100644 --- a/packages/server/modules/viewer/tests/integration/savedViewsCrud.graph.spec.ts +++ b/packages/server/modules/viewer/tests/integration/savedViewsCrud.graph.spec.ts @@ -74,7 +74,7 @@ import { Roles, WorkspacePlans } from '@speckle/shared' import { ProjectNotEnoughPermissionsError, SavedViewNoAccessError, - WorkspacePlanNoFeatureAccessError + WorkspaceNoAccessError } from '@speckle/shared/authz' import * as ViewerRoute from '@speckle/shared/viewer/route' import { resourceBuilder } from '@speckle/shared/viewer/route' @@ -121,7 +121,6 @@ const fakeViewerState = (overrides?: PartialDeep { - const res = await createSavedView( - buildCreateInput({ - projectId: myLackingProject.id, - resourceIdString: 'abc' - }) - ) - expect(res).to.haveGraphQLErrors({ code: ForbiddenError.code }) - expect(res.data?.projectMutations.savedViewMutations.createView).to.not.be.ok - }) - - it('should fail with ForbiddenError to create a saved view group if user lacks access (free plan)', async () => { - const resourceIds = ViewerRoute.resourceBuilder().addModel( - myLackingProject.id - ) - const resourceIdString = resourceIds.toString() - - const res = await createSavedViewGroup({ - input: { - projectId: myLackingProject.id, - resourceIdString, - groupName: 'Should Not Work' - } - }) - - expect(res).to.haveGraphQLErrors({ code: ForbiddenError.code }) - expect(res.data?.projectMutations.savedViewMutations.createGroup).to.not.be.ok - }) - - it('should fail with ForbiddenError to create a saved view if user lacks access (free plan)', async () => { - const resourceIds = ViewerRoute.resourceBuilder().addModel( - myLackingProject.id - ) - const resourceIdString = resourceIds.toString() - const viewerState = fakeViewerState({ - projectId: myLackingProject.id, - resources: { - request: { - resourceIdString - } - } - }) - - const res = await createSavedView( - buildCreateInput({ - projectId: myLackingProject.id, - resourceIdString, - viewerState - }) - ) - - expect(res).to.haveGraphQLErrors({ code: ForbiddenError.code }) - expect(res.data?.projectMutations.savedViewMutations.createView).to.not.be.ok - }) - it('should support dedicated auth policy check', async () => { const res = await canCreateSavedView({ projectId: myLackingProject.id @@ -419,7 +363,7 @@ const fakeViewerState = (overrides?: PartialDeep { const result = await sut({ projectId: 'project-id', - feature: WorkspacePlanFeatures.SavedViews + feature: WorkspacePlanFeatures.HideSpeckleBranding }) expect(result).toBeAuthOKResult() @@ -990,7 +990,7 @@ describe('ensureCanUseProjectWorkspacePlanFeatureFragment', () => { const result = await sut({ projectId: 'project-id', - feature: WorkspacePlanFeatures.SavedViews + feature: WorkspacePlanFeatures.HideSpeckleBranding }) expect(result).toBeAuthErrorResult({ @@ -1008,7 +1008,7 @@ describe('ensureCanUseProjectWorkspacePlanFeatureFragment', () => { const result = await sut({ projectId: 'project-id', - feature: WorkspacePlanFeatures.SavedViews + feature: WorkspacePlanFeatures.HideSpeckleBranding }) expect(result).toBeAuthErrorResult({ @@ -1026,7 +1026,7 @@ describe('ensureCanUseProjectWorkspacePlanFeatureFragment', () => { const result = await sut({ projectId: 'project-id', - feature: WorkspacePlanFeatures.SavedViews, + feature: WorkspacePlanFeatures.HideSpeckleBranding, allowUnworkspaced: true }) @@ -1044,7 +1044,7 @@ describe('ensureCanUseProjectWorkspacePlanFeatureFragment', () => { const result = await sut({ projectId: 'project-id', - feature: WorkspacePlanFeatures.SavedViews + feature: WorkspacePlanFeatures.HideSpeckleBranding }) expect(result).toBeAuthErrorResult({ diff --git a/packages/shared/src/authz/fragments/savedViews.spec.ts b/packages/shared/src/authz/fragments/savedViews.spec.ts index 68684726f..22254094f 100644 --- a/packages/shared/src/authz/fragments/savedViews.spec.ts +++ b/packages/shared/src/authz/fragments/savedViews.spec.ts @@ -20,8 +20,7 @@ import { SavedViewNoAccessError, SavedViewNotFoundError, UngroupedSavedViewGroupLockError, - WorkspaceNoAccessError, - WorkspacePlanNoFeatureAccessError + WorkspaceNoAccessError } from '../domain/authErrors.js' import { nanoid } from 'nanoid' @@ -191,11 +190,17 @@ describe('ensureCanAccessSavedViewFragment', () => { ) it.each(['read', 'write'])( - 'fails when workspace plan is too cheap (%s)', + 'succeeds to %s even on free plan', async (access) => { const sut = buildWorkspaceSUT({ getWorkspacePlan: getWorkspacePlanFake({ - name: 'team' + name: 'free' + }), + getSavedView: getSavedViewFake({ + id: savedViewId, + projectId, + visibility: SavedViewVisibility.public, + authorId: userId }) }) @@ -205,9 +210,7 @@ describe('ensureCanAccessSavedViewFragment', () => { savedViewId, access }) - expect(result).toBeAuthErrorResult({ - code: WorkspacePlanNoFeatureAccessError.code - }) + expect(result).toBeAuthOKResult() } ) @@ -413,27 +416,6 @@ describe('ensureCanAccessSavedViewGroupFragment', () => { }) }) - it.each(['read', 'write'])( - 'fails when workspace plan is too cheap (%s)', - async (access) => { - const sut = buildWorkspaceSUT({ - getWorkspacePlan: getWorkspacePlanFake({ - name: 'team' - }) - }) - - const result = await sut({ - userId, - projectId, - savedViewGroupId, - access - }) - expect(result).toBeAuthErrorResult({ - code: WorkspacePlanNoFeatureAccessError.code - }) - } - ) - it.each(['read', 'write'])( 'fails if view doesnt exist (%s)', async (access) => { diff --git a/packages/shared/src/authz/fragments/workspaces.spec.ts b/packages/shared/src/authz/fragments/workspaces.spec.ts index 34b3eb796..09112e94b 100644 --- a/packages/shared/src/authz/fragments/workspaces.spec.ts +++ b/packages/shared/src/authz/fragments/workspaces.spec.ts @@ -347,7 +347,7 @@ describe('ensureCanUseWorkspacePlanFeatureFragment', () => { const result = await sut({ workspaceId: cryptoRandomString({ length: 10 }), - feature: WorkspacePlanFeatures.SavedViews + feature: WorkspacePlanFeatures.HideSpeckleBranding }) expect(result).toBeOKResult() @@ -362,7 +362,7 @@ describe('ensureCanUseWorkspacePlanFeatureFragment', () => { const result = await sut({ workspaceId: cryptoRandomString({ length: 10 }), - feature: WorkspacePlanFeatures.SavedViews + feature: WorkspacePlanFeatures.HideSpeckleBranding }) expect(result).toBeAuthErrorResult({ @@ -380,7 +380,7 @@ describe('ensureCanUseWorkspacePlanFeatureFragment', () => { const result = await sut({ workspaceId: cryptoRandomString({ length: 10 }), - feature: WorkspacePlanFeatures.SavedViews + feature: WorkspacePlanFeatures.HideSpeckleBranding }) expect(result).toBeAuthErrorResult({ @@ -395,7 +395,7 @@ describe('ensureCanUseWorkspacePlanFeatureFragment', () => { const result = await sut({ workspaceId: cryptoRandomString({ length: 10 }), - feature: WorkspacePlanFeatures.SavedViews + feature: WorkspacePlanFeatures.HideSpeckleBranding }) expect(result).toBeAuthErrorResult({ @@ -413,7 +413,7 @@ describe('ensureCanUseWorkspacePlanFeatureFragment', () => { const result = await sut({ workspaceId: cryptoRandomString({ length: 10 }), - feature: WorkspacePlanFeatures.SavedViews + feature: WorkspacePlanFeatures.HideSpeckleBranding }) expect(result).toBeAuthErrorResult({ diff --git a/packages/shared/src/authz/policies/project/savedViews/canCreate.spec.ts b/packages/shared/src/authz/policies/project/savedViews/canCreate.spec.ts index c55da40d4..895303f36 100644 --- a/packages/shared/src/authz/policies/project/savedViews/canCreate.spec.ts +++ b/packages/shared/src/authz/policies/project/savedViews/canCreate.spec.ts @@ -14,10 +14,9 @@ import { ProjectNotEnoughPermissionsError, ServerNoAccessError, WorkspaceNoAccessError, - WorkspacePlanNoFeatureAccessError, WorkspaceReadOnlyError } from '../../../domain/authErrors.js' -import { PaidWorkspacePlans } from '../../../../workspaces/index.js' +import { WorkspacePlans } from '../../../../workspaces/index.js' const buildSUT = (overrides?: OverridesOf) => canCreateSavedViewPolicy({ @@ -71,7 +70,7 @@ describe('canCreateSavedViewPolicy', () => { id: 'workspace-id' }), getWorkspacePlan: getWorkspacePlanFake({ - name: PaidWorkspacePlans.Pro + name: WorkspacePlans.Pro }), getWorkspaceSsoProvider: async () => ({ providerId: 'provider-id' @@ -153,10 +152,10 @@ describe('canCreateSavedViewPolicy', () => { }) }) - it('fails if not on pro/business plan', async () => { + it('succeeds even on free plan', async () => { const canCreate = buildWorkspaceSUT({ getWorkspacePlan: getWorkspacePlanFake({ - name: PaidWorkspacePlans.Team + name: WorkspacePlans.Free }) }) @@ -164,15 +163,13 @@ describe('canCreateSavedViewPolicy', () => { userId: 'user-id', projectId: 'project-id' }) - expect(result).toBeAuthErrorResult({ - code: WorkspacePlanNoFeatureAccessError.code - }) + expect(result).toBeAuthOKResult() }) it('fails if workspace readonly', async () => { const canCreate = buildWorkspaceSUT({ getWorkspacePlan: getWorkspacePlanFake({ - name: PaidWorkspacePlans.Pro, + name: WorkspacePlans.Pro, status: 'canceled' }) }) diff --git a/packages/shared/src/workspaces/helpers/features.ts b/packages/shared/src/workspaces/helpers/features.ts index 3d66b8b94..bca4a54a8 100644 --- a/packages/shared/src/workspaces/helpers/features.ts +++ b/packages/shared/src/workspaces/helpers/features.ts @@ -134,135 +134,137 @@ export const WorkspacePaidPlanConfigs: (params: { featureFlags: Partial | undefined }) => { [plan in PaidWorkspacePlans]: WorkspacePlanConfig -} = (params) => ({ - [PaidWorkspacePlans.Team]: { - plan: PaidWorkspacePlans.Team, - features: [...baseFeatures], - limits: { - projectCount: 5, - modelCount: 25, - versionsHistory: { value: 30, unit: 'day' }, - commentHistory: { value: 30, unit: 'day' } - } - }, - [PaidWorkspacePlans.TeamUnlimited]: { - plan: PaidWorkspacePlans.TeamUnlimited, - features: [...baseFeatures], - limits: { - projectCount: null, - modelCount: null, - versionsHistory: { value: 30, unit: 'day' }, - commentHistory: { value: 30, unit: 'day' } - } - }, - [PaidWorkspacePlans.Pro]: { - plan: PaidWorkspacePlans.Pro, - features: [ - ...baseFeatures, - WorkspacePlanFeatures.DomainSecurity, - WorkspacePlanFeatures.SSO, - WorkspacePlanFeatures.CustomDataRegion, - WorkspacePlanFeatures.HideSpeckleBranding, - ...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED - ? [WorkspacePlanFeatures.SavedViews] - : []) - ], - limits: { - projectCount: 10, - modelCount: 50, - versionsHistory: null, - commentHistory: null - } - }, - [PaidWorkspacePlans.ProUnlimited]: { - plan: PaidWorkspacePlans.ProUnlimited, - features: [ - ...baseFeatures, - WorkspacePlanFeatures.DomainSecurity, - WorkspacePlanFeatures.SSO, - WorkspacePlanFeatures.CustomDataRegion, - WorkspacePlanFeatures.HideSpeckleBranding, - ...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED - ? [WorkspacePlanFeatures.SavedViews] - : []) - ], - limits: { - projectCount: null, - modelCount: null, - versionsHistory: null, - commentHistory: null +} = (params) => { + const finalBaseFeatures = [ + ...baseFeatures, + ...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED + ? [WorkspacePlanFeatures.SavedViews] + : []) + ] + + return { + [PaidWorkspacePlans.Team]: { + plan: PaidWorkspacePlans.Team, + features: [...finalBaseFeatures], + limits: { + projectCount: 5, + modelCount: 25, + versionsHistory: { value: 30, unit: 'day' }, + commentHistory: { value: 30, unit: 'day' } + } + }, + [PaidWorkspacePlans.TeamUnlimited]: { + plan: PaidWorkspacePlans.TeamUnlimited, + features: [...finalBaseFeatures], + limits: { + projectCount: null, + modelCount: null, + versionsHistory: { value: 30, unit: 'day' }, + commentHistory: { value: 30, unit: 'day' } + } + }, + [PaidWorkspacePlans.Pro]: { + plan: PaidWorkspacePlans.Pro, + features: [ + ...finalBaseFeatures, + WorkspacePlanFeatures.DomainSecurity, + WorkspacePlanFeatures.SSO, + WorkspacePlanFeatures.CustomDataRegion, + WorkspacePlanFeatures.HideSpeckleBranding + ], + limits: { + projectCount: 10, + modelCount: 50, + versionsHistory: null, + commentHistory: null + } + }, + [PaidWorkspacePlans.ProUnlimited]: { + plan: PaidWorkspacePlans.ProUnlimited, + features: [ + ...finalBaseFeatures, + WorkspacePlanFeatures.DomainSecurity, + WorkspacePlanFeatures.SSO, + WorkspacePlanFeatures.CustomDataRegion, + WorkspacePlanFeatures.HideSpeckleBranding + ], + limits: { + projectCount: null, + modelCount: null, + versionsHistory: null, + commentHistory: null + } } } -}) +} export const WorkspaceUnpaidPlanConfigs: (params: { featureFlags: Partial | undefined }) => { [plan in UnpaidWorkspacePlans]: WorkspacePlanConfig -} = (params) => ({ - [UnpaidWorkspacePlans.Enterprise]: { - plan: UnpaidWorkspacePlans.Enterprise, - features: [ - ...baseFeatures, - WorkspacePlanFeatures.DomainSecurity, - WorkspacePlanFeatures.SSO, - WorkspacePlanFeatures.CustomDataRegion, - WorkspacePlanFeatures.HideSpeckleBranding, - WorkspacePlanFeatures.ExclusiveMembership, - ...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED - ? [WorkspacePlanFeatures.SavedViews] - : []) - ], - limits: unlimited - }, - [UnpaidWorkspacePlans.Unlimited]: { - plan: UnpaidWorkspacePlans.Unlimited, - features: [ - ...baseFeatures, - WorkspacePlanFeatures.DomainSecurity, - WorkspacePlanFeatures.SSO, - WorkspacePlanFeatures.CustomDataRegion, - WorkspacePlanFeatures.HideSpeckleBranding, - WorkspacePlanFeatures.ExclusiveMembership, - ...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED - ? [WorkspacePlanFeatures.SavedViews] - : []) - ], - limits: unlimited - }, - [UnpaidWorkspacePlans.Academia]: { - plan: UnpaidWorkspacePlans.Academia, - features: [ - ...baseFeatures, - WorkspacePlanFeatures.DomainSecurity, - WorkspacePlanFeatures.SSO, - WorkspacePlanFeatures.CustomDataRegion, - WorkspacePlanFeatures.HideSpeckleBranding, - ...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED - ? [WorkspacePlanFeatures.SavedViews] - : []) - ], - limits: unlimited - }, - [UnpaidWorkspacePlans.TeamUnlimitedInvoiced]: { - ...WorkspacePaidPlanConfigs(params).teamUnlimited, - plan: UnpaidWorkspacePlans.TeamUnlimitedInvoiced - }, - [UnpaidWorkspacePlans.ProUnlimitedInvoiced]: { - ...WorkspacePaidPlanConfigs(params).proUnlimited, - plan: UnpaidWorkspacePlans.ProUnlimitedInvoiced - }, - [UnpaidWorkspacePlans.Free]: { - plan: UnpaidWorkspacePlans.Free, - features: baseFeatures, - limits: { - projectCount: 1, - modelCount: 5, - versionsHistory: { value: 7, unit: 'day' }, - commentHistory: { value: 7, unit: 'day' } +} = (params) => { + const finalBaseFeatures = [ + ...baseFeatures, + ...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED + ? [WorkspacePlanFeatures.SavedViews] + : []) + ] + return { + [UnpaidWorkspacePlans.Enterprise]: { + plan: UnpaidWorkspacePlans.Enterprise, + features: [ + ...finalBaseFeatures, + WorkspacePlanFeatures.DomainSecurity, + WorkspacePlanFeatures.SSO, + WorkspacePlanFeatures.CustomDataRegion, + WorkspacePlanFeatures.HideSpeckleBranding, + WorkspacePlanFeatures.ExclusiveMembership + ], + limits: unlimited + }, + [UnpaidWorkspacePlans.Unlimited]: { + plan: UnpaidWorkspacePlans.Unlimited, + features: [ + ...finalBaseFeatures, + WorkspacePlanFeatures.DomainSecurity, + WorkspacePlanFeatures.SSO, + WorkspacePlanFeatures.CustomDataRegion, + WorkspacePlanFeatures.HideSpeckleBranding, + WorkspacePlanFeatures.ExclusiveMembership + ], + limits: unlimited + }, + [UnpaidWorkspacePlans.Academia]: { + plan: UnpaidWorkspacePlans.Academia, + features: [ + ...finalBaseFeatures, + WorkspacePlanFeatures.DomainSecurity, + WorkspacePlanFeatures.SSO, + WorkspacePlanFeatures.CustomDataRegion, + WorkspacePlanFeatures.HideSpeckleBranding + ], + limits: unlimited + }, + [UnpaidWorkspacePlans.TeamUnlimitedInvoiced]: { + ...WorkspacePaidPlanConfigs(params).teamUnlimited, + plan: UnpaidWorkspacePlans.TeamUnlimitedInvoiced + }, + [UnpaidWorkspacePlans.ProUnlimitedInvoiced]: { + ...WorkspacePaidPlanConfigs(params).proUnlimited, + plan: UnpaidWorkspacePlans.ProUnlimitedInvoiced + }, + [UnpaidWorkspacePlans.Free]: { + plan: UnpaidWorkspacePlans.Free, + features: finalBaseFeatures, + limits: { + projectCount: 1, + modelCount: 5, + versionsHistory: { value: 7, unit: 'day' }, + commentHistory: { value: 7, unit: 'day' } + } } } -}) +} export const WorkspacePlanConfigs = (params: { featureFlags: Partial | undefined diff --git a/tsconfig.json b/tsconfig.json index 24731045f..d946318dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,3 @@ { - /* load each package separately, rather than as one giant progream */ - "files": [], - "references": [ - { "path": "packages/fileimport-service" }, - { "path": "packages/frontend-2" }, - { "path": "packages/monitor-deployment" }, - { "path": "packages/objectloader" }, - { "path": "packages/objectloader2" }, - { "path": "packages/objectsender" }, - { "path": "packages/preview-frontend" }, - { "path": "packages/preview-service" }, - { "path": "packages/server" }, - { "path": "packages/shared" }, - { "path": "packages/tailwind-theme" }, - { "path": "packages/ui-components" }, - { "path": "packages/ui-components-nuxt" }, - { "path": "packages/viewer" }, - { "path": "packages/viewer-sandbox" }, - { "path": "packages/webhook-service" } - /* …add all other packages listed in workspace.code-workspace */ - ] + "files": [] } diff --git a/workspace.code-workspace b/workspace.code-workspace index 95140e340..1bdac3a62 100644 --- a/workspace.code-workspace +++ b/workspace.code-workspace @@ -111,6 +111,7 @@ "Prorotation" ], "typescript.tsserver.maxTsServerMemory": 8192, + "typescript.disableAutomaticTypeAcquisition": true, "tailwindCSS.experimental.configFile": { "packages/frontend-2/tailwind.config.cjs": "packages/frontend-2/**" },