feat: Added "Private vaults" as a Labs feature. Can be enabled from Preferences > General > Labs (#2562)
This commit is contained in:
@@ -41,6 +41,7 @@ export class NativeFeatureIdentifier extends ValueObject<NativeFeatureIdentifier
|
|||||||
|
|
||||||
Clipper: 'org.standardnotes.clipper',
|
Clipper: 'org.standardnotes.clipper',
|
||||||
|
|
||||||
|
Vaults: 'org.standardnotes.vaults',
|
||||||
SharedVaults: 'org.standardnotes.shared-vaults',
|
SharedVaults: 'org.standardnotes.shared-vaults',
|
||||||
|
|
||||||
DeprecatedMarkdownVisualEditor: 'org.standardnotes.markdown-visual-editor',
|
DeprecatedMarkdownVisualEditor: 'org.standardnotes.markdown-visual-editor',
|
||||||
@@ -74,4 +75,4 @@ export class NativeFeatureIdentifier extends ValueObject<NativeFeatureIdentifier
|
|||||||
/**
|
/**
|
||||||
* Identifier for standalone filesafe instance offered as legacy installable via extensions-server
|
* Identifier for standalone filesafe instance offered as legacy installable via extensions-server
|
||||||
*/
|
*/
|
||||||
export const ExperimentalFeatures = []
|
export const ExperimentalFeatures = [NativeFeatureIdentifier.TYPES.Vaults]
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
|
import { RoleName } from '@standardnotes/domain-core'
|
||||||
import { AnyFeatureDescription } from '../Feature/AnyFeatureDescription'
|
import { AnyFeatureDescription } from '../Feature/AnyFeatureDescription'
|
||||||
|
import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier'
|
||||||
|
import { PermissionName } from '../Permission/PermissionName'
|
||||||
|
|
||||||
export function experimentalFeatures(): AnyFeatureDescription[] {
|
export function experimentalFeatures(): AnyFeatureDescription[] {
|
||||||
return []
|
return [
|
||||||
|
{
|
||||||
|
name: 'Private vaults',
|
||||||
|
description: 'Private vaults allow you to store notes, files and tags into separate, encrypted vaults.',
|
||||||
|
availableInRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
|
identifier: NativeFeatureIdentifier.TYPES.Vaults,
|
||||||
|
permission_name: PermissionName.Vaults,
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,5 +38,6 @@ export enum PermissionName {
|
|||||||
SubscriptionSharing = 'server:subscription-sharing',
|
SubscriptionSharing = 'server:subscription-sharing',
|
||||||
SuperEditor = 'editor:super-editor',
|
SuperEditor = 'editor:super-editor',
|
||||||
Clipper = 'app:clipper',
|
Clipper = 'app:clipper',
|
||||||
|
Vaults = 'app:vaults',
|
||||||
SharedVaults = 'server:shared-vaults',
|
SharedVaults = 'server:shared-vaults',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{application.featuresController.isEntitledToVaults() && (
|
{application.featuresController.isVaultsEnabled() && (
|
||||||
<AddToVaultMenuOption
|
<AddToVaultMenuOption
|
||||||
iconClassName={iconClass}
|
iconClassName={iconClass}
|
||||||
items={selectedFiles}
|
items={selectedFiles}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const VaultSelectionButton = ({ isMobileNavigation = false }: { isMobileNavigati
|
|||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const toggleMenu = () => setIsOpen(!isOpen)
|
const toggleMenu = () => setIsOpen(!isOpen)
|
||||||
|
|
||||||
if (!application.featuresController.isEntitledToVaults()) {
|
if (!application.featuresController.isVaultsEnabled()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
)}
|
)}
|
||||||
<HorizontalSeparator classes="my-2" />
|
<HorizontalSeparator classes="my-2" />
|
||||||
|
|
||||||
{application.featuresController.isEntitledToVaults() && (
|
{application.featuresController.isVaultsEnabled() && (
|
||||||
<AddToVaultMenuOption iconClassName={iconClass} items={notes} disabled={!hasAdminPermissionForAllSharedNotes} />
|
<AddToVaultMenuOption iconClassName={iconClass} items={notes} disabled={!hasAdminPermissionForAllSharedNotes} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
|||||||
continueTitle: DEFAULT_CONTINUE_TITLE,
|
continueTitle: DEFAULT_CONTINUE_TITLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.application.featuresController.isEntitledToVaults()) {
|
if (props.application.featuresController.isVaultsEnabled()) {
|
||||||
this.state = {
|
this.state = {
|
||||||
...baseState,
|
...baseState,
|
||||||
lockContinue: true,
|
lockContinue: true,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class PreferencesSessionController {
|
|||||||
? PREFERENCES_MENU_ITEMS.slice()
|
? PREFERENCES_MENU_ITEMS.slice()
|
||||||
: READY_PREFERENCES_MENU_ITEMS.slice()
|
: READY_PREFERENCES_MENU_ITEMS.slice()
|
||||||
|
|
||||||
if (application.featuresController.isEntitledToVaults()) {
|
if (application.featuresController.isVaultsEnabled()) {
|
||||||
menuItems.push({ id: 'vaults', label: 'Vaults', icon: 'safe-square', order: 5 })
|
menuItems.push({ id: 'vaults', label: 'Vaults', icon: 'safe-square', order: 5 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const Vaults = observer(() => {
|
|||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
|
|
||||||
const hasAccount = application.hasAccount()
|
const hasAccount = application.hasAccount()
|
||||||
|
const isSharedVaultsEnabled = application.featuresController.isEntitledToSharedVaults()
|
||||||
|
|
||||||
const [vaults, setVaults] = useState<VaultListingInterface[]>([])
|
const [vaults, setVaults] = useState<VaultListingInterface[]>([])
|
||||||
const [canCreateMoreVaults, setCanCreateMoreVaults] = useState(true)
|
const [canCreateMoreVaults, setCanCreateMoreVaults] = useState(true)
|
||||||
@@ -163,7 +164,7 @@ const Vaults = observer(() => {
|
|||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
)}
|
)}
|
||||||
{hasAccount && (
|
{hasAccount && isSharedVaultsEnabled && (
|
||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<Title>Contacts</Title>
|
<Title>Contacts</Title>
|
||||||
@@ -180,7 +181,7 @@ const Vaults = observer(() => {
|
|||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
)}
|
)}
|
||||||
{hasAccount && (
|
{hasAccount && isSharedVaultsEnabled && (
|
||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<Title>CollaborationID</Title>
|
<Title>CollaborationID</Title>
|
||||||
@@ -235,13 +236,15 @@ const Vaults = observer(() => {
|
|||||||
{canCreateMoreVaults ? (
|
{canCreateMoreVaults ? (
|
||||||
<div className="mt-2.5 flex gap-3">
|
<div className="mt-2.5 flex gap-3">
|
||||||
<Button label="Create Vault" onClick={createNewVault} />
|
<Button label="Create Vault" onClick={createNewVault} />
|
||||||
{hasAccount && <Button label="Create Shared Vault" onClick={createNewSharedVault} />}
|
{hasAccount && isSharedVaultsEnabled && (
|
||||||
|
<Button label="Create Shared Vault" onClick={createNewSharedVault} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3.5">
|
<div className="mt-3.5">
|
||||||
<NoProSubscription
|
<NoProSubscription
|
||||||
application={application}
|
application={application}
|
||||||
text={<span>Please upgrade in order to increase your shared vault limit.</span>}
|
text={<span>Please upgrade in order to increase your vault limit.</span>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ type Props = {
|
|||||||
const VaultItem = ({ vault }: Props) => {
|
const VaultItem = ({ vault }: Props) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
|
|
||||||
|
const canEnableCollaboration = application.hasAccount() && application.featuresController.isEntitledToSharedVaults()
|
||||||
|
|
||||||
const [isInviteModalOpen, setIsAddContactModalOpen] = useState(false)
|
const [isInviteModalOpen, setIsAddContactModalOpen] = useState(false)
|
||||||
const closeInviteModal = () => setIsAddContactModalOpen(false)
|
const closeInviteModal = () => setIsAddContactModalOpen(false)
|
||||||
|
|
||||||
@@ -128,7 +130,7 @@ const VaultItem = ({ vault }: Props) => {
|
|||||||
{isCurrentUserAdmin ? (
|
{isCurrentUserAdmin ? (
|
||||||
vault.isSharedVaultListing() ? (
|
vault.isSharedVaultListing() ? (
|
||||||
<Button colorStyle="info" label="Invite Contacts" onClick={openInviteModal} />
|
<Button colorStyle="info" label="Invite Contacts" onClick={openInviteModal} />
|
||||||
) : application.hasAccount() ? (
|
) : canEnableCollaboration ? (
|
||||||
<Button colorStyle="info" label="Enable Collaboration" onClick={convertToSharedVault} />
|
<Button colorStyle="info" label="Enable Collaboration" onClick={convertToSharedVault} />
|
||||||
) : null
|
) : null
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const Navigation = forwardRef<HTMLDivElement, Props>(({ application, className,
|
|||||||
)}
|
)}
|
||||||
<PreferencesButton openPreferences={() => application.preferencesController.openPreferences()} />
|
<PreferencesButton openPreferences={() => application.preferencesController.openPreferences()} />
|
||||||
<QuickSettingsButton application={application} isMobileNavigation />
|
<QuickSettingsButton application={application} isMobileNavigation />
|
||||||
{application.featuresController.isEntitledToVaults() && <VaultSelectionButton isMobileNavigation />}
|
{application.featuresController.isVaultsEnabled() && <VaultSelectionButton isMobileNavigation />}
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
|
|||||||
iconGridClassName="max-h-30"
|
iconGridClassName="max-h-30"
|
||||||
/>
|
/>
|
||||||
<HorizontalSeparator classes="my-2" />
|
<HorizontalSeparator classes="my-2" />
|
||||||
{application.featuresController.isEntitledToVaults() && (
|
{application.featuresController.isVaultsEnabled() && (
|
||||||
<AddToVaultMenuOption iconClassName="mr-2 text-neutral" items={[selectedTag]} />
|
<AddToVaultMenuOption iconClassName="mr-2 text-neutral" items={[selectedTag]} />
|
||||||
)}
|
)}
|
||||||
<MenuItem className={'justify-between py-1.5'} onClick={onClickStar}>
|
<MenuItem className={'justify-between py-1.5'} onClick={onClickStar}>
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const AddToVaultMenuOption = ({
|
|||||||
setIsSubMenuOpen((isOpen) => !isOpen)
|
setIsSubMenuOpen((isOpen) => !isOpen)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!application.featuresController.isEntitledToVaults()) {
|
if (!application.featuresController.isVaultsEnabled()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,12 @@ export class FeaturesController extends AbstractViewController implements Intern
|
|||||||
return status === FeatureStatus.Entitled
|
return status === FeatureStatus.Entitled
|
||||||
}
|
}
|
||||||
|
|
||||||
isEntitledToVaults(): boolean {
|
isVaultsEnabled(): boolean {
|
||||||
|
const enabled = this.features.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.Vaults)
|
||||||
|
return featureTrunkVaultsEnabled() || enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
isEntitledToSharedVaults(): boolean {
|
||||||
const status = this.features.getFeatureStatus(
|
const status = this.features.getFeatureStatus(
|
||||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SharedVaults).getValue(),
|
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SharedVaults).getValue(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ describe('LinkingController', () => {
|
|||||||
application.getPreference = jest.fn()
|
application.getPreference = jest.fn()
|
||||||
application.addSingleEventObserver = jest.fn()
|
application.addSingleEventObserver = jest.fn()
|
||||||
application.sync.sync = jest.fn()
|
application.sync.sync = jest.fn()
|
||||||
application.featuresController.isEntitledToVaults = jest.fn().mockReturnValue(true)
|
application.featuresController.isVaultsEnabled = jest.fn().mockReturnValue(true)
|
||||||
|
application.featuresController.isEntitledToSharedVaults = jest.fn().mockReturnValue(true)
|
||||||
|
|
||||||
Object.defineProperty(application, 'items', { value: {} as jest.Mocked<ItemManagerInterface> })
|
Object.defineProperty(application, 'items', { value: {} as jest.Mocked<ItemManagerInterface> })
|
||||||
|
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export class LinkingController extends AbstractViewController implements Interna
|
|||||||
const linkNoteAndFile = async (note: SNNote, file: FileItem) => {
|
const linkNoteAndFile = async (note: SNNote, file: FileItem) => {
|
||||||
const updatedFile = await this.mutator.associateFileWithNote(file, note)
|
const updatedFile = await this.mutator.associateFileWithNote(file, note)
|
||||||
|
|
||||||
if (this.featuresController.isEntitledToVaults()) {
|
if (this.featuresController.isVaultsEnabled()) {
|
||||||
if (updatedFile) {
|
if (updatedFile) {
|
||||||
const noteVault = this.vaults.getItemVault(note)
|
const noteVault = this.vaults.getItemVault(note)
|
||||||
const fileVault = this.vaults.getItemVault(updatedFile)
|
const fileVault = this.vaults.getItemVault(updatedFile)
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export function featureTrunkEnabled(trunk: FeatureTrunkName): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function featureTrunkVaultsEnabled(): boolean {
|
export function featureTrunkVaultsEnabled(): boolean {
|
||||||
return InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)
|
// return InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export function featureTrunkHomeServerEnabled(): boolean {
|
export function featureTrunkHomeServerEnabled(): boolean {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const useItemVaultInfo = (item: DecryptedItemInterface): ItemVaultInfo =>
|
|||||||
const [sharedByContact, setSharedByContact] = useState<TrustedContactInterface>()
|
const [sharedByContact, setSharedByContact] = useState<TrustedContactInterface>()
|
||||||
|
|
||||||
const updateInfo = useCallback(() => {
|
const updateInfo = useCallback(() => {
|
||||||
if (!application.featuresController.isEntitledToVaults()) {
|
if (!application.featuresController.isVaultsEnabled()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user