From a074aedd65e40560a804414288b8076301639253 Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Mon, 1 Sep 2025 10:25:10 +0300 Subject: [PATCH] feat: enable saved views for all workspace plans (#5343) * feat: enable saved views for all workspace plans * more test fixes --- .../viewer/saved-views/plan_upsell.webp | Bin 8638 -> 0 bytes .../components/viewer/saved-views/Panel.vue | 1 - .../viewer/saved-views/PlanUpsell.vue | 20 -- .../integration/savedViewsCrud.graph.spec.ts | 64 +---- .../src/authz/fragments/projects.spec.ts | 10 +- .../src/authz/fragments/savedViews.spec.ts | 38 +-- .../src/authz/fragments/workspaces.spec.ts | 10 +- .../project/savedViews/canCreate.spec.ts | 15 +- .../shared/src/workspaces/helpers/features.ts | 242 +++++++++--------- tsconfig.json | 22 +- workspace.code-workspace | 1 + 11 files changed, 154 insertions(+), 269 deletions(-) delete mode 100644 packages/frontend-2/assets/images/viewer/saved-views/plan_upsell.webp delete mode 100644 packages/frontend-2/components/viewer/saved-views/PlanUpsell.vue 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 73812efd254e0ecb36cae089e441f745dd2b34a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8638 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 R_en#-D{PPtBM|`LzX1G7v@!qy 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/**" },