feat: saved views disclaimers (#5265)
This commit is contained in:
committed by
GitHub
parent
6801e106b8
commit
4650936bf0
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<ViewerLayoutSidePanel disable-scrollbar @close="$emit('close')">
|
||||
<ViewerLayoutSidePanel disable-scrollbar class="relative" @close="$emit('close')">
|
||||
<template #title>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>Views</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<div v-if="!isLowerPlan" class="flex items-center gap-0.5">
|
||||
<FormButton
|
||||
v-if="false"
|
||||
size="sm"
|
||||
@@ -38,34 +38,53 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="px-4 pt-2">
|
||||
<ViewerButtonGroup>
|
||||
<ViewerButtonGroupButton
|
||||
v-for="viewsType in Object.values(ViewsType)"
|
||||
:key="viewsType"
|
||||
:is-active="selectedViewsType === viewsType"
|
||||
class="grow"
|
||||
@click="() => (selectedViewsType = viewsType)"
|
||||
>
|
||||
<span class="text-body-2xs text-foreground px-2 py-1">
|
||||
{{ viewsTypeLabels[viewsType] }}
|
||||
</span>
|
||||
</ViewerButtonGroupButton>
|
||||
</ViewerButtonGroup>
|
||||
</div>
|
||||
<div class="text-body-sm flex-1 min-h-0 overflow-y-auto simple-scrollbar">
|
||||
<ViewerSavedViewsPanelGroups
|
||||
v-model:selected-group-id="selectedGroupId"
|
||||
:views-type="selectedViewsType"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="!isLowerPlan">
|
||||
<div class="px-4 pt-2">
|
||||
<ViewerButtonGroup>
|
||||
<ViewerButtonGroupButton
|
||||
v-for="viewsType in Object.values(ViewsType)"
|
||||
:key="viewsType"
|
||||
:is-active="selectedViewsType === viewsType"
|
||||
class="grow"
|
||||
@click="() => (selectedViewsType = viewsType)"
|
||||
>
|
||||
<span class="text-body-2xs text-foreground px-2 py-1">
|
||||
{{ viewsTypeLabels[viewsType] }}
|
||||
</span>
|
||||
</ViewerButtonGroupButton>
|
||||
</ViewerButtonGroup>
|
||||
</div>
|
||||
<div class="text-body-sm flex-1 min-h-0 overflow-y-auto simple-scrollbar">
|
||||
<ViewerSavedViewsPanelGroups
|
||||
v-model:selected-group-id="selectedGroupId"
|
||||
:views-type="selectedViewsType"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="isViewerSeat && !hideViewerSeatDisclaimer"
|
||||
class="absolute bottom-0 left-0 right-0 p-2"
|
||||
>
|
||||
<CommonPromoAlert
|
||||
title="Save your views"
|
||||
text="With an editor seat, unlock the option to save your own views."
|
||||
:button="{ title: 'Learn more' }"
|
||||
show-closer
|
||||
@close="hideViewerSeatDisclaimer = true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<ViewerSavedViewsPlanUpsell v-else />
|
||||
</ViewerLayoutSidePanel>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useMutationLoading } from '@vue/apollo-composable'
|
||||
import { Search, FolderPlus, Plus } from 'lucide-vue-next'
|
||||
import { useSynchronizedCookie } from '~/lib/common/composables/reactiveCookie'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { SavedViewVisibility } from '~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
SavedViewVisibility,
|
||||
WorkspaceSeatType
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
useCreateSavedView,
|
||||
useCreateSavedViewGroup
|
||||
@@ -81,6 +100,11 @@ graphql(`
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
workspace {
|
||||
id
|
||||
seatType
|
||||
planSupportsSavedViews: hasAccessToFeature(featureName: savedViews)
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -101,10 +125,20 @@ const isLoading = useMutationLoading()
|
||||
|
||||
const selectedViewsType = ref<ViewsType>(ViewsType.Personal)
|
||||
const selectedGroupId = ref<string | null>(null)
|
||||
const hideViewerSeatDisclaimer = useSynchronizedCookie<boolean>(
|
||||
'hideViewerSeatSavedViewsDisclaimer',
|
||||
{
|
||||
default: () => false
|
||||
}
|
||||
)
|
||||
|
||||
const canCreateViewOrGroup = computed(
|
||||
() => project.value?.permissions.canCreateSavedView
|
||||
)
|
||||
const isViewerSeat = computed(
|
||||
() => project.value?.workspace?.seatType === WorkspaceSeatType.Viewer
|
||||
)
|
||||
const isLowerPlan = computed(() => !project.value?.workspace?.planSupportsSavedViews)
|
||||
|
||||
const onAddView = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<img src="~/assets/images/viewer/saved-views/plan_upsell.webp" alt="Saved Views" />
|
||||
<div>
|
||||
<div class="text-foreground text-body font-semibold">Save custom views</div>
|
||||
<div class="text-body-2xs font-medium text-foreground-2">
|
||||
<p class="pb-3">Upgrade to a business plan to save, organise and present</p>
|
||||
<ul class="flex flex-col gap-2 list-disc list-inside">
|
||||
<li>It's cool</li>
|
||||
<li>It's nice</li>
|
||||
<li>It's got enough spice</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<FormButton size="sm">Upgrade</FormButton>
|
||||
<FormButton size="sm" color="outline">Learn more</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -206,7 +206,8 @@ watch(
|
||||
groups,
|
||||
(newGroups) => {
|
||||
if (newGroups.length && !selectedGroupId.value) {
|
||||
selectedGroupId.value = newGroups[0].id
|
||||
// open default group, if any
|
||||
selectedGroupId.value = newGroups.find((g) => g.isUngroupedViewsGroup)?.id || null
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { FeatureFlags } from '@speckle/shared/environment/featureFlags'
|
||||
|
||||
/**
|
||||
* IMPORTANT: Don't use this directly in Vue templates that may render in SSR, cause this may cause the backend API origin to be rendered instead of the clientside one,
|
||||
* at least until the app finishes hydrating. If people click on links based on this too early, they may end up in the wrong place.
|
||||
@@ -17,3 +19,8 @@ export const useApiOrigin = (
|
||||
|
||||
return apiOrigin
|
||||
}
|
||||
|
||||
export const useFeatureFlags = (): FeatureFlags => {
|
||||
const { public: featureFlags } = useRuntimeConfig()
|
||||
return featureFlags
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ type Documents = {
|
||||
"\n fragment ViewerResourcesLimitAlert_Project on Project {\n id\n workspaceId\n workspace {\n id\n slug\n ...ViewerResourcesWorkspaceLimitAlert_Workspace\n }\n ...WorkspaceMoveProject_Project\n }\n": typeof types.ViewerResourcesLimitAlert_ProjectFragmentDoc,
|
||||
"\n fragment ViewerResourcesPersonalLimitAlert_Project on Project {\n id\n ...WorkspaceMoveProject_Project\n }\n": typeof types.ViewerResourcesPersonalLimitAlert_ProjectFragmentDoc,
|
||||
"\n fragment ViewerResourcesWorkspaceLimitAlert_Workspace on Workspace {\n id\n slug\n }\n": typeof types.ViewerResourcesWorkspaceLimitAlert_WorkspaceFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ViewerSavedViewsPanel_ProjectFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n seatType\n planSupportsSavedViews: hasAccessToFeature(featureName: savedViews)\n }\n }\n": typeof types.ViewerSavedViewsPanel_ProjectFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelGroups_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n ...ViewerSavedViewsPanelViewsGroup_Project\n }\n": typeof types.ViewerSavedViewsPanelGroups_ProjectFragmentDoc,
|
||||
"\n query ViewerSavedViewsPanelGroups_SavedViewGroups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelGroups_Project\n }\n }\n": typeof types.ViewerSavedViewsPanelGroups_SavedViewGroupsDocument,
|
||||
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n }\n": typeof types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
|
||||
@@ -670,7 +670,7 @@ const documents: Documents = {
|
||||
"\n fragment ViewerResourcesLimitAlert_Project on Project {\n id\n workspaceId\n workspace {\n id\n slug\n ...ViewerResourcesWorkspaceLimitAlert_Workspace\n }\n ...WorkspaceMoveProject_Project\n }\n": types.ViewerResourcesLimitAlert_ProjectFragmentDoc,
|
||||
"\n fragment ViewerResourcesPersonalLimitAlert_Project on Project {\n id\n ...WorkspaceMoveProject_Project\n }\n": types.ViewerResourcesPersonalLimitAlert_ProjectFragmentDoc,
|
||||
"\n fragment ViewerResourcesWorkspaceLimitAlert_Workspace on Workspace {\n id\n slug\n }\n": types.ViewerResourcesWorkspaceLimitAlert_WorkspaceFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ViewerSavedViewsPanel_ProjectFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n seatType\n planSupportsSavedViews: hasAccessToFeature(featureName: savedViews)\n }\n }\n": types.ViewerSavedViewsPanel_ProjectFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelGroups_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n ...ViewerSavedViewsPanelViewsGroup_Project\n }\n": types.ViewerSavedViewsPanelGroups_ProjectFragmentDoc,
|
||||
"\n query ViewerSavedViewsPanelGroups_SavedViewGroups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelGroups_Project\n }\n }\n": types.ViewerSavedViewsPanelGroups_SavedViewGroupsDocument,
|
||||
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n }\n": types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
|
||||
@@ -1647,7 +1647,7 @@ export function graphql(source: "\n fragment ViewerResourcesWorkspaceLimitAlert
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n seatType\n planSupportsSavedViews: hasAccessToFeature(featureName: savedViews)\n }\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n seatType\n planSupportsSavedViews: hasAccessToFeature(featureName: savedViews)\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -160,6 +160,7 @@ enum WorkspaceFeatureName {
|
||||
hideSpeckleBranding
|
||||
workspaceDataRegionSpecificity
|
||||
exclusiveMembership
|
||||
savedViews
|
||||
}
|
||||
|
||||
type WorkspacePlanPrice {
|
||||
|
||||
@@ -5305,6 +5305,7 @@ export const WorkspaceFeatureName = {
|
||||
ExclusiveMembership: 'exclusiveMembership',
|
||||
HideSpeckleBranding: 'hideSpeckleBranding',
|
||||
OidcSso: 'oidcSso',
|
||||
SavedViews: 'savedViews',
|
||||
WorkspaceDataRegionSpecificity: 'workspaceDataRegionSpecificity'
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -598,7 +598,7 @@ export const updateSavedViewRecordFactory =
|
||||
[SavedViews.col.id]: id,
|
||||
[SavedViews.col.projectId]: projectId
|
||||
})
|
||||
.update(update, '*')
|
||||
.update({ ...update, updatedAt: new Date() }, '*')
|
||||
|
||||
return updatedView || undefined
|
||||
}
|
||||
@@ -636,7 +636,13 @@ export const updateSavedViewGroupRecordFactory =
|
||||
[SavedViewGroups.col.id]: groupId,
|
||||
[SavedViewGroups.col.projectId]: projectId
|
||||
})
|
||||
.update(update, '*')
|
||||
.update(
|
||||
{
|
||||
...update,
|
||||
updatedAt: new Date()
|
||||
},
|
||||
'*'
|
||||
)
|
||||
|
||||
return updatedGroup
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
"./package.json": "./package.json",
|
||||
"./pinoPrettyTransport.cjs": "./pinoPrettyTransport.cjs",
|
||||
"./environment": "./src/environment/index.ts",
|
||||
"./environment/featureFlags": "./src/environment/featureFlags.ts",
|
||||
"./environment/db": "./src/environment/db.ts",
|
||||
"./environment/node": "./src/environment/node.ts",
|
||||
"./environment/multiRegionConfig": "./src/environment/db.ts",
|
||||
@@ -143,6 +144,16 @@
|
||||
"default": "./dist/commonjs/environment/index.js"
|
||||
}
|
||||
},
|
||||
"./environment/featureFlags": {
|
||||
"import": {
|
||||
"types": "./dist/esm/environment/featureFlags.d.ts",
|
||||
"default": "./dist/esm/environment/featureFlags.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/environment/featureFlags.d.ts",
|
||||
"default": "./dist/commonjs/environment/featureFlags.js"
|
||||
}
|
||||
},
|
||||
"./environment/db": {
|
||||
"import": {
|
||||
"types": "./dist/esm/environment/db.d.ts",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WorkspaceRoles, WorkspaceSeatType } from '../../../core/constants.js'
|
||||
import { FeatureFlags } from '../../../environment/index.js'
|
||||
import { FeatureFlags } from '../../../environment/featureFlags.js'
|
||||
import { WorkspaceLimits } from '../../../workspaces/helpers/limits.js'
|
||||
import { WorkspacePlan } from '../../../workspaces/index.js'
|
||||
import { UserContext, WorkspaceContext } from '../context.js'
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* IMPORTANT: This should not have any node-only code (e.g. Zod)
|
||||
*/
|
||||
|
||||
export type FeatureFlags = {
|
||||
FF_AUTOMATE_MODULE_ENABLED: boolean
|
||||
FF_GENDOAI_MODULE_ENABLED: boolean
|
||||
FF_WORKSPACES_MODULE_ENABLED: boolean
|
||||
FF_WORKSPACES_SSO_ENABLED: boolean
|
||||
FF_GATEKEEPER_MODULE_ENABLED: boolean
|
||||
FF_BILLING_INTEGRATION_ENABLED: boolean
|
||||
FF_WORKSPACES_MULTI_REGION_ENABLED: boolean
|
||||
FF_FORCE_ONBOARDING: boolean
|
||||
FF_MOVE_PROJECT_REGION_ENABLED: boolean
|
||||
FF_NO_PERSONAL_EMAILS_ENABLED: boolean
|
||||
FF_RETRY_ERRORED_PREVIEWS_ENABLED: boolean
|
||||
FF_PERSONAL_PROJECTS_LIMITS_ENABLED: boolean
|
||||
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: boolean
|
||||
FF_RHINO_FILE_IMPORTER_ENABLED: boolean
|
||||
FF_BACKGROUND_JOBS_ENABLED: boolean
|
||||
FF_LEGACY_FILE_IMPORTS_ENABLED: boolean
|
||||
FF_LEGACY_IFC_IMPORTER_ENABLED: boolean
|
||||
FF_EXPERIMENTAL_IFC_IMPORTER_ENABLED: boolean
|
||||
FF_ACC_INTEGRATION_ENABLED: boolean
|
||||
FF_SAVED_VIEWS_ENABLED: boolean
|
||||
FF_USERS_INVITE_SCOPE_IS_PUBLIC: boolean
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, afterEach, beforeEach } from 'vitest'
|
||||
import { type FeatureFlags, parseFeatureFlags } from './index.js'
|
||||
import { parseFeatureFlags } from './index.js'
|
||||
import { FeatureFlags } from './featureFlags.js'
|
||||
|
||||
const originalDisableAllFfs = process.env.DISABLE_ALL_FFS || ''
|
||||
const originalEnableAllFfs = process.env.ENABLE_ALL_FFS || ''
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { has } from '#lodash'
|
||||
import { parseEnv } from 'znv'
|
||||
import { z } from 'zod'
|
||||
import { FeatureFlags } from './featureFlags.js'
|
||||
|
||||
// Convenience variable to override below individual feature flags, which has the effect of setting all to 'false' (disabled)
|
||||
// Takes precedence over ENABLE_ALL_FFS
|
||||
@@ -180,30 +181,6 @@ export const parseFeatureFlags = (
|
||||
|
||||
let parsedFlags: FeatureFlags | undefined
|
||||
|
||||
export type FeatureFlags = {
|
||||
FF_AUTOMATE_MODULE_ENABLED: boolean
|
||||
FF_GENDOAI_MODULE_ENABLED: boolean
|
||||
FF_WORKSPACES_MODULE_ENABLED: boolean
|
||||
FF_WORKSPACES_SSO_ENABLED: boolean
|
||||
FF_GATEKEEPER_MODULE_ENABLED: boolean
|
||||
FF_BILLING_INTEGRATION_ENABLED: boolean
|
||||
FF_WORKSPACES_MULTI_REGION_ENABLED: boolean
|
||||
FF_FORCE_ONBOARDING: boolean
|
||||
FF_MOVE_PROJECT_REGION_ENABLED: boolean
|
||||
FF_NO_PERSONAL_EMAILS_ENABLED: boolean
|
||||
FF_RETRY_ERRORED_PREVIEWS_ENABLED: boolean
|
||||
FF_PERSONAL_PROJECTS_LIMITS_ENABLED: boolean
|
||||
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: boolean
|
||||
FF_RHINO_FILE_IMPORTER_ENABLED: boolean
|
||||
FF_BACKGROUND_JOBS_ENABLED: boolean
|
||||
FF_LEGACY_FILE_IMPORTS_ENABLED: boolean
|
||||
FF_LEGACY_IFC_IMPORTER_ENABLED: boolean
|
||||
FF_EXPERIMENTAL_IFC_IMPORTER_ENABLED: boolean
|
||||
FF_ACC_INTEGRATION_ENABLED: boolean
|
||||
FF_SAVED_VIEWS_ENABLED: boolean
|
||||
FF_USERS_INVITE_SCOPE_IS_PUBLIC: boolean
|
||||
}
|
||||
|
||||
export function getFeatureFlags(): FeatureFlags {
|
||||
//@ts-expect-error this way, the parse function typing is a lot better
|
||||
if (!parsedFlags) parsedFlags = parseFeatureFlags(process.env, { forceInputs: false })
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
WorkspaceSsoProvider,
|
||||
WorkspaceSsoSession
|
||||
} from '../authz/domain/workspaces/types.js'
|
||||
import { FeatureFlags, parseFeatureFlags } from '../environment/index.js'
|
||||
import { parseFeatureFlags } from '../environment/index.js'
|
||||
import { mapValues } from 'lodash'
|
||||
import { WorkspacePlan } from '../workspaces/index.js'
|
||||
import { TIME_MS } from '../core/index.js'
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
SavedViewGroup,
|
||||
SavedViewVisibility
|
||||
} from '../authz/domain/savedViews/types.js'
|
||||
import { FeatureFlags } from '../environment/featureFlags.js'
|
||||
|
||||
export const fakeGetFactory =
|
||||
<T extends Record<string, unknown>>(defaults: () => T) =>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
WorkspacePlans
|
||||
} from './plans.js'
|
||||
import type { MaybeNullOrUndefined } from '../../core/helpers/utilityTypes.js'
|
||||
import { FeatureFlags } from '../../environment/index.js'
|
||||
import { FeatureFlags } from '../../environment/featureFlags.js'
|
||||
|
||||
/**
|
||||
* WORKSPACE FEATURES
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"lodash": "^4.0.0",
|
||||
"lodash-es": "^4.0.0",
|
||||
"lucide-vue-next": "^0.537.0",
|
||||
"nanoid": "^3.0.0",
|
||||
"v3-infinite-loading": "^1.2.2",
|
||||
"vue-advanced-cropper": "^2.8.8",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// AGENT WRITE HERE:
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import PromoAlert from '~~/src/components/common/PromoAlert.vue'
|
||||
|
||||
const meta: Meta<typeof PromoAlert> = {
|
||||
component: PromoAlert,
|
||||
args: {
|
||||
title: 'Upgrade your workspace',
|
||||
text: 'Unlock advanced features and collaboration tools by upgrading your plan.',
|
||||
button: { title: 'Learn more', to: 'https://speckle.systems' }
|
||||
},
|
||||
argTypes: {
|
||||
title: { control: 'text' },
|
||||
text: { control: 'text' },
|
||||
button: { control: 'object' },
|
||||
showCloser: { control: 'boolean' }
|
||||
}
|
||||
}
|
||||
export default meta
|
||||
|
||||
interface PromoAlertArgs {
|
||||
title?: string
|
||||
text?: string
|
||||
button?: { to?: string; title: string }
|
||||
showCloser?: boolean
|
||||
}
|
||||
|
||||
const render = (args: PromoAlertArgs) => ({
|
||||
components: { PromoAlert },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<div class="max-w-sm h-full">
|
||||
<PromoAlert v-bind="args" @click="args.click" @close="args.close" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
type Story = StoryObj<typeof PromoAlert>
|
||||
|
||||
export const Default: Story = { render }
|
||||
|
||||
export const WithoutLink: Story = { render, args: { button: { title: 'Learn more' } } }
|
||||
|
||||
export const WithoutButton: Story = { render, args: { button: undefined } }
|
||||
|
||||
export const TitleOnly: Story = { render, args: { text: undefined, button: undefined } }
|
||||
|
||||
export const ButtonOnly: Story = {
|
||||
render,
|
||||
args: { title: undefined, text: undefined, button: { title: 'Upgrade' } }
|
||||
}
|
||||
|
||||
export const CustomClick: Story = {
|
||||
render,
|
||||
args: { button: { title: 'Trigger action' } }
|
||||
}
|
||||
|
||||
export const WithCloser: Story = { render, args: { showCloser: true } }
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-foundation-page shadow-sm flex flex-col gap-y-1 sm:gap-y-2 border border-outline-3 rounded-lg py-2 px-3 sm:p-4 select-none"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<h6
|
||||
v-if="title"
|
||||
class="text-body-xs sm:text-heading-sm font-medium text-foreground"
|
||||
>
|
||||
{{ title }}
|
||||
</h6>
|
||||
<X
|
||||
v-if="showCloser"
|
||||
v-keyboard-clickable
|
||||
class="h-4 w-4 cursor-pointer focus:outline-none"
|
||||
@click="$emit('close', $event)"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="text" class="text-body-2xs sm:text-body-xs text-foreground-2 !leading-5">
|
||||
{{ text }}
|
||||
</p>
|
||||
<div class="flex justify-end">
|
||||
<FormButton
|
||||
v-if="button"
|
||||
size="sm"
|
||||
class="mt-1"
|
||||
:to="button.to"
|
||||
:target="button.to ? '_blank' : undefined"
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
{{ button.title }}
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FormButton from '~~/src/components/form/Button.vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { vKeyboardClickable } from '~~/src/directives/accessibility'
|
||||
|
||||
defineEmits<{
|
||||
click: [e: MouseEvent]
|
||||
close: [e: MouseEvent]
|
||||
}>()
|
||||
|
||||
defineProps<{
|
||||
title?: string
|
||||
text?: string
|
||||
button?: { to?: string; title: string }
|
||||
showCloser?: boolean
|
||||
}>()
|
||||
</script>
|
||||
@@ -111,10 +111,12 @@ export { vKeyboardClickable } from '~~/src/directives/accessibility'
|
||||
export { useAvatarSizeClasses } from '~~/src/composables/user/avatar'
|
||||
export type { UserAvatarSize } from '~~/src/composables/user/avatar'
|
||||
import CommonProgressBar from '~~/src/components/common/ProgressBar.vue'
|
||||
import CommonPromoAlert from '~~/src/components/common/PromoAlert.vue'
|
||||
import FormRange from '~~/src/components/form/Range.vue'
|
||||
import type { FormRadioGroupItem } from '~~/src/helpers/common/components'
|
||||
|
||||
export {
|
||||
CommonPromoAlert,
|
||||
MissingFileExtensionError,
|
||||
ForbiddenFileTypeError,
|
||||
FileTooLargeError,
|
||||
|
||||
Reference in New Issue
Block a user