mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: refactor vault component helpers to use dedicated functions for options retrieval
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,45 +28,56 @@ interface TypeOption {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const CREATE_TYPE_OPTIONS: TypeOption[] = [
|
||||
{ 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 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 }> = [
|
||||
{ value: 'edited', label: t('txt_sort_last_edited') },
|
||||
{ value: 'created', label: t('txt_sort_created') },
|
||||
{ value: 'name', label: t('txt_sort_name') },
|
||||
];
|
||||
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 }> = [
|
||||
{ value: 0, label: t('txt_text') },
|
||||
{ value: 1, label: t('txt_hidden') },
|
||||
{ value: 2, label: t('txt_boolean') },
|
||||
];
|
||||
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 const WEBSITE_MATCH_OPTIONS: Array<{ value: number | null; label: string }> = [
|
||||
{ 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') },
|
||||
{ value: 3, label: t('txt_uri_match_exact') },
|
||||
{ value: 5, label: t('txt_uri_match_never') },
|
||||
{ value: 2, label: t('txt_uri_match_starts_with') },
|
||||
{ value: 4, label: t('txt_uri_match_regular_expression') },
|
||||
];
|
||||
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 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') },
|
||||
{ value: 3, label: t('txt_uri_match_exact') },
|
||||
{ value: 5, label: t('txt_uri_match_never') },
|
||||
{ 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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user