feat: refactor vault component helpers to use dedicated functions for options retrieval

This commit is contained in:
shuaiplus
2026-04-29 15:28:23 +08:00
parent 85147e1569
commit 9c5fbda374
8 changed files with 93 additions and 48 deletions
+3 -2
View File
@@ -1,6 +1,6 @@
import ConfirmDialog from '@/components/ConfirmDialog';
import type { CustomFieldType, Folder } from '@/lib/types';
import { FIELD_TYPE_OPTIONS, toBooleanFieldValue } from '@/components/vault/vault-page-helpers';
import { getFieldTypeOptions, toBooleanFieldValue } from '@/components/vault/vault-page-helpers';
import { t } from '@/lib/i18n';
interface VaultDialogsProps {
@@ -61,6 +61,7 @@ interface VaultDialogsProps {
}
export default function VaultDialogs(props: VaultDialogsProps) {
const fieldTypeOptions = getFieldTypeOptions();
return (
<>
<ConfirmDialog
@@ -75,7 +76,7 @@ export default function VaultDialogs(props: VaultDialogsProps) {
<label className="field">
<span>{t('txt_field_type')}</span>
<select className="input" value={props.fieldType} onInput={(e) => props.onFieldTypeChange(Number((e.currentTarget as HTMLSelectElement).value) as CustomFieldType)}>
{FIELD_TYPE_OPTIONS.map((option) => (
{fieldTypeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
+6 -4
View File
@@ -21,13 +21,13 @@ import { CSS } from '@dnd-kit/utilities';
import type { Cipher, Folder, VaultDraft, VaultDraftField } from '@/lib/types';
import { t } from '@/lib/i18n';
import {
CREATE_TYPE_OPTIONS,
cipherTypeLabel,
createEmptyLoginUri,
formatAttachmentSize,
formatHistoryTime,
getCreateTypeOptions,
getWebsiteMatchOptions,
toBooleanFieldValue,
WEBSITE_MATCH_OPTIONS,
} from '@/components/vault/vault-page-helpers';
interface VaultEditorProps {
@@ -77,6 +77,7 @@ interface SortableWebsiteRowProps {
}
function SortableWebsiteRow(props: SortableWebsiteRowProps) {
const websiteMatchOptions = getWebsiteMatchOptions();
const { attributes, listeners, setActivatorNodeRef, setNodeRef, transform, transition, isDragging } = useSortable({
id: props.id,
});
@@ -117,7 +118,7 @@ function SortableWebsiteRow(props: SortableWebsiteRowProps) {
props.onUpdateMatch(props.index, raw === '' ? null : Number(raw));
}}
>
{WEBSITE_MATCH_OPTIONS.map((option) => (
{websiteMatchOptions.map((option) => (
<option key={`website-match-${String(option.value)}`} value={option.value == null ? '' : String(option.value)}>
{option.label}
</option>
@@ -134,6 +135,7 @@ function SortableWebsiteRow(props: SortableWebsiteRowProps) {
}
export default function VaultEditor(props: VaultEditorProps) {
const createTypeOptions = getCreateTypeOptions();
const uriIdSeedRef = useRef(0);
const [uriItemIds, setUriItemIds] = useState<string[]>([]);
const [activeUriId, setActiveUriId] = useState<string | null>(null);
@@ -232,7 +234,7 @@ export default function VaultEditor(props: VaultEditorProps) {
if (nextType === 5) props.onSeedSshDefaults();
}}
>
{CREATE_TYPE_OPTIONS.map((option) => (
{createTypeOptions.map((option) => (
<option key={option.type} value={option.type}>
{option.label}
</option>
@@ -6,9 +6,9 @@ import LoadingState from '@/components/LoadingState';
import type { Cipher } from '@/lib/types';
import { t } from '@/lib/i18n';
import {
CREATE_TYPE_OPTIONS,
CreateTypeIcon,
VAULT_SORT_OPTIONS,
getCreateTypeOptions,
getVaultSortOptions,
VaultListIcon,
type SidebarFilter,
type VaultSortMode,
@@ -106,6 +106,8 @@ const CipherListItem = memo(function CipherListItem(props: CipherListItemProps)
});
export default function VaultListPanel(props: VaultListPanelProps) {
const createTypeOptions = getCreateTypeOptions();
const vaultSortOptions = getVaultSortOptions();
const createMenu = (
<div className="create-menu-wrap mobile-fab-wrap" ref={props.createMenuRef}>
<button
@@ -119,7 +121,7 @@ export default function VaultListPanel(props: VaultListPanelProps) {
</button>
{props.createMenuOpen && (
<div className="create-menu">
{CREATE_TYPE_OPTIONS.map((option) => (
{createTypeOptions.map((option) => (
<button key={option.type} type="button" className="create-menu-item" onClick={() => props.onStartCreate(option.type)}>
<CreateTypeIcon type={option.type} />
<span>{option.label}</span>
@@ -171,7 +173,7 @@ export default function VaultListPanel(props: VaultListPanelProps) {
</button>
{props.sortMenuOpen && (
<div className="sort-menu">
{VAULT_SORT_OPTIONS.map((option) => (
{vaultSortOptions.map((option) => (
<button
key={option.value}
type="button"
+3 -2
View File
@@ -21,7 +21,7 @@ import {
} from 'lucide-preact';
import type { Folder } from '@/lib/types';
import { t } from '@/lib/i18n';
import { FOLDER_SORT_OPTIONS, type SidebarFilter, type VaultSortMode } from '@/components/vault/vault-page-helpers';
import { getFolderSortOptions, type SidebarFilter, type VaultSortMode } from '@/components/vault/vault-page-helpers';
interface VaultSidebarProps {
folders: Folder[];
@@ -43,6 +43,7 @@ interface VaultSidebarProps {
}
export default function VaultSidebar(props: VaultSidebarProps) {
const folderSortOptions = getFolderSortOptions();
const nameCollator = useMemo(
() => new Intl.Collator(undefined, { sensitivity: 'base', numeric: true }),
[]
@@ -143,7 +144,7 @@ export default function VaultSidebar(props: VaultSidebarProps) {
</button>
{props.folderSortMenuOpen && (
<div className="sort-menu">
{FOLDER_SORT_OPTIONS.map((option) => (
{folderSortOptions.map((option) => (
<button
key={option.value}
type="button"
@@ -28,37 +28,47 @@ interface TypeOption {
label: string;
}
export const CREATE_TYPE_OPTIONS: TypeOption[] = [
export function getCreateTypeOptions(): TypeOption[] {
return [
{ type: 1, label: t('txt_login') },
{ type: 3, label: t('txt_card') },
{ type: 4, label: t('txt_identity') },
{ type: 2, label: t('txt_note') },
{ type: 5, label: t('txt_ssh_key') },
];
}
export const VAULT_SORT_STORAGE_KEY = 'nodewarden.vault.sort.v1';
export const FOLDER_SORT_STORAGE_KEY = 'nodewarden.folder-sort.v1';
export const MOBILE_LAYOUT_QUERY = '(max-width: 1180px)';
export const VAULT_LIST_ROW_HEIGHT = 74;
export const VAULT_LIST_OVERSCAN = 10;
export const VAULT_SORT_OPTIONS: Array<{ value: VaultSortMode; label: string }> = [
{ value: 'edited', label: t('txt_sort_last_edited') },
{ value: 'created', label: t('txt_sort_created') },
{ value: 'name', label: t('txt_sort_name') },
];
export const FOLDER_SORT_OPTIONS: Array<{ value: VaultSortMode; label: string }> = [
export function getVaultSortOptions(): Array<{ value: VaultSortMode; label: string }> {
return [
{ value: 'edited', label: t('txt_sort_last_edited') },
{ value: 'created', label: t('txt_sort_created') },
{ value: 'name', label: t('txt_sort_name') },
];
}
export const FIELD_TYPE_OPTIONS: Array<{ value: CustomFieldType; label: string }> = [
export function getFolderSortOptions(): Array<{ value: VaultSortMode; label: string }> {
return [
{ value: 'edited', label: t('txt_sort_last_edited') },
{ value: 'created', label: t('txt_sort_created') },
{ value: 'name', label: t('txt_sort_name') },
];
}
export function getFieldTypeOptions(): Array<{ value: CustomFieldType; label: string }> {
return [
{ value: 0, label: t('txt_text') },
{ value: 1, label: t('txt_hidden') },
{ value: 2, label: t('txt_boolean') },
];
}
export const WEBSITE_MATCH_OPTIONS: Array<{ value: number | null; label: string }> = [
export function getWebsiteMatchOptions(): Array<{ value: number | null; label: string }> {
return [
{ value: null, label: t('txt_uri_match_default_base_domain') },
{ value: 0, label: t('txt_uri_match_base_domain') },
{ value: 1, label: t('txt_uri_match_host') },
@@ -67,6 +77,7 @@ export const WEBSITE_MATCH_OPTIONS: Array<{ value: number | null; label: string
{ value: 2, label: t('txt_uri_match_starts_with') },
{ value: 4, label: t('txt_uri_match_regular_expression') },
];
}
export const TOTP_PERIOD_SECONDS = 30;
export const TOTP_RING_RADIUS = 14;
@@ -156,7 +167,7 @@ export function createEmptyLoginUri(): VaultDraftLoginUri {
export function websiteMatchLabel(value: number | null | undefined): string {
const normalized = typeof value === 'number' && Number.isFinite(value) ? value : null;
return WEBSITE_MATCH_OPTIONS.find((option) => option.value === normalized)?.label || t('txt_uri_match_default_base_domain');
return getWebsiteMatchOptions().find((option) => option.value === normalized)?.label || t('txt_uri_match_default_base_domain');
}
function valueOrFallback(value: string | null | undefined): string {
+22 -1
View File
@@ -134,7 +134,28 @@ export function loadProfileSnapshot(email?: string | null): Profile | null {
export function saveProfileSnapshot(profile: Profile | null): void {
if (!profile) return;
localStorage.setItem(PROFILE_SNAPSHOT_KEY, JSON.stringify(stripProfileSecrets(profile)));
const nextSnapshot = stripProfileSecrets(profile);
try {
const rawExisting = localStorage.getItem(PROFILE_SNAPSHOT_KEY);
if (rawExisting) {
const existing = stripProfileSecrets(JSON.parse(rawExisting) as Profile);
if (
existing
&& existing.email === nextSnapshot?.email
&& existing.role === 'admin'
&& nextSnapshot?.role !== 'admin'
) {
localStorage.setItem(PROFILE_SNAPSHOT_KEY, JSON.stringify({
...nextSnapshot,
role: 'admin',
}));
return;
}
}
} catch {
// Fall back to writing the normalized snapshot below.
}
localStorage.setItem(PROFILE_SNAPSHOT_KEY, JSON.stringify(nextSnapshot));
}
export function clearProfileSnapshot(): void {
+6 -3
View File
@@ -144,7 +144,7 @@ function decodeAccessTokenClaims(accessToken: string): AccessTokenClaims {
}
}
function buildTransientProfile(token: TokenSuccess, email: string): Profile {
function buildTransientProfile(token: TokenSuccess, email: string, fallbackProfile: Profile | null = null): Profile {
const claims = decodeAccessTokenClaims(token.access_token);
const normalizedEmail = String(claims.email || email || '').trim().toLowerCase();
const accountKeys = token.accountKeys ?? token.AccountKeys ?? null;
@@ -154,9 +154,11 @@ function buildTransientProfile(token: TokenSuccess, email: string): Profile {
name: String(claims.name || normalizedEmail || ''),
key: String(token.Key || ''),
privateKey: token.PrivateKey ?? null,
role: 'user',
role: fallbackProfile?.role === 'admin' ? 'admin' : 'user',
premium: !!claims.premium,
accountKeys,
masterPasswordHint: fallbackProfile?.masterPasswordHint ?? null,
publicKey: fallbackProfile?.publicKey ?? null,
object: 'profile',
};
}
@@ -256,6 +258,7 @@ export async function completeLogin(
masterKey: Uint8Array
): Promise<CompletedLogin> {
const normalizedEmail = email.trim().toLowerCase();
const fallbackProfile = loadProfileSnapshot(normalizedEmail);
const baseSession: SessionState = {
accessToken: token.access_token,
refreshToken: token.refresh_token,
@@ -266,7 +269,7 @@ export async function completeLogin(
() => baseSession,
() => {}
);
const profile = buildTransientProfile(token, normalizedEmail);
const profile = buildTransientProfile(token, normalizedEmail, fallbackProfile);
if (!profile.key) {
throw new Error('Missing profile key');
}
+4
View File
@@ -249,6 +249,9 @@
.list-panel {
@apply min-h-0 overflow-auto p-2;
/* Virtualized rows update top/bottom spacers while scrolling. Chrome's scroll anchoring
can fight those updates and produce subtle up/down jitter, so disable it here. */
overflow-anchor: none;
}
.list-item {
@@ -256,6 +259,7 @@
background: rgba(249, 251, 255, 0.9);
border-color: var(--line);
contain: paint;
overflow-anchor: none;
}
.list-item::before {