feat: version 2.14.0 -- compartir grupo completo y destruir con cascada
Contraseñas: "Compartir grupo" aplica el mismo share de solo lectura a todas las credenciales de un grupo de una vez (el grupo sigue sin ser una unidad de permiso -- es una accion de conveniencia). El grupo y cada credencial se marcan con el icono de compartido. "Destruir grupo" ahora tiene dos opciones: solo desagrupar (como antes) o destruir tambien las N credenciales, con advertencia explicita del conteo. Fix de paso en ComboBox (afecta a toda la app): el panel se cerraba solo al abrirse si el trigger estaba cerca del borde de un contenedor con scroll, por un micro-scroll automatico del navegador al enfocar el boton. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import { useDebouncedValue } from '../../hooks/useDebouncedValue.js';
|
||||
import {
|
||||
getVaultStatus, createVaultPin, unlockVault, changeVaultPin, forgotVaultPin, setVaultToken,
|
||||
listVaultEntries, getVaultEntry, createVaultEntry, updateVaultEntry, deleteVaultEntry,
|
||||
listVaultGroups, createVaultGroup, updateVaultGroup, deleteVaultGroup,
|
||||
listVaultGroups, createVaultGroup, updateVaultGroup, deleteVaultGroup, shareVaultGroup,
|
||||
listEntryShares, addEntryShare, removeEntryShare,
|
||||
} from '../../services/vault.js';
|
||||
import { listUsers } from '../../services/users.js';
|
||||
@@ -102,6 +102,7 @@ export default function ContrasenasPage() {
|
||||
const [groupPopupOpen, setGroupPopupOpen] = useState(false);
|
||||
const [editingGroup, setEditingGroup] = useState(null);
|
||||
const [groupForm, setGroupForm] = useState(EMPTY_GROUP_FORM);
|
||||
const [groupSharingWith, setGroupSharingWith] = useState('');
|
||||
|
||||
const [changingPin, setChangingPin] = useState(false);
|
||||
const [pinChangeForm, setPinChangeForm] = useState({ current: '', next: '', next2: '' });
|
||||
@@ -319,12 +320,24 @@ export default function ContrasenasPage() {
|
||||
function openEditGroup(group) {
|
||||
setEditingGroup(group);
|
||||
setGroupForm({ name: group.name, iconKey: group.icon_key || '' });
|
||||
setGroupSharingWith('');
|
||||
setGroupPopupOpen(true);
|
||||
}
|
||||
|
||||
function closeGroupPopup() {
|
||||
setGroupPopupOpen(false);
|
||||
setEditingGroup(null);
|
||||
setGroupSharingWith('');
|
||||
}
|
||||
|
||||
async function shareGroup() {
|
||||
if (!groupSharingWith || !editingGroup) return;
|
||||
await runAction(async () => {
|
||||
await shareVaultGroup(editingGroup.id, Number(groupSharingWith));
|
||||
setGroupSharingWith('');
|
||||
pushToast({ tone: 'success', message: t('common_saved') });
|
||||
await Promise.all([loadGroups(), loadEntries()]);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveGroup() {
|
||||
@@ -341,17 +354,20 @@ export default function ContrasenasPage() {
|
||||
});
|
||||
}
|
||||
|
||||
function confirmDeleteGroup(group) {
|
||||
function confirmDeleteGroup(group, cascade) {
|
||||
const message = cascade
|
||||
? `${t('vault_confirm_destroy_group_cascade_prefix')}${group.entry_count}${t('vault_confirm_destroy_group_cascade_suffix')}`
|
||||
: t('vault_confirm_destroy_group_msg');
|
||||
openDialog({
|
||||
tone: 'error',
|
||||
title: t('vault_confirm_destroy_title'),
|
||||
message: t('vault_confirm_destroy_group_msg'),
|
||||
message,
|
||||
actions: [
|
||||
{ label: t('common_cancel'), variant: 'standard' },
|
||||
{
|
||||
label: t('vault_destroy'), variant: 'danger',
|
||||
onClick: () => runAction(async () => {
|
||||
await deleteVaultGroup(group.id);
|
||||
await deleteVaultGroup(group.id, cascade);
|
||||
if (activeGroupId === group.id) setActiveGroupId(null);
|
||||
await Promise.all([loadGroups(), loadEntries()]);
|
||||
pushToast({ tone: 'success', message: t('common_saved') });
|
||||
@@ -446,9 +462,16 @@ export default function ContrasenasPage() {
|
||||
<button className="icon-btn row-menu-btn" onClick={onClick}><MoreVertical size={16} /></button>
|
||||
)}>
|
||||
<FlyoutItem icon={Pencil} onClick={() => openEditGroup(group)}>{t('common_edit')}</FlyoutItem>
|
||||
<FlyoutItem icon={Trash2} danger onClick={() => confirmDeleteGroup(group)}>{t('vault_destroy')}</FlyoutItem>
|
||||
<FlyoutItem icon={Share2} onClick={() => openEditGroup(group)}>{t('vault_share_group_action')}</FlyoutItem>
|
||||
<FlyoutItem icon={Trash2} danger onClick={() => confirmDeleteGroup(group, false)}>{t('vault_destroy_group_action')}</FlyoutItem>
|
||||
<FlyoutItem icon={Trash2} danger onClick={() => confirmDeleteGroup(group, true)}>{t('vault_destroy_group_cascade_action')}</FlyoutItem>
|
||||
</Flyout>
|
||||
</div>
|
||||
{group.shared_entries_count > 0 && (
|
||||
<span className="vault-card-shared-badge" title={t('vault_share_group_action')}>
|
||||
<Share2 size={13} strokeWidth={2.5} />
|
||||
</span>
|
||||
)}
|
||||
<GroupIcon iconKey={group.icon_key} />
|
||||
<div className="vault-card-title">{group.name}</div>
|
||||
<div className="vault-card-url">{group.entry_count} {t('vault_group_entries_count')}</div>
|
||||
@@ -655,7 +678,7 @@ export default function ContrasenasPage() {
|
||||
title={editingGroup ? editingGroup.name : t('vault_group_new')}
|
||||
onClose={closeGroupPopup}
|
||||
width={380}
|
||||
height={300}
|
||||
height={editingGroup ? 480 : 300}
|
||||
>
|
||||
<Field label={t('vault_group_name')}>
|
||||
<input className="input" autoFocus value={groupForm.name} onChange={(e) => setGroupForm((f) => ({ ...f, name: e.target.value }))} />
|
||||
@@ -686,6 +709,28 @@ export default function ContrasenasPage() {
|
||||
<Button variant="accent" icon={Save} onClick={saveGroup} disabled={!groupForm.name.trim()}>{t('common_save')}</Button>
|
||||
<Button variant="standard" icon={X} onClick={closeGroupPopup}>{t('common_cancel')}</Button>
|
||||
</div>
|
||||
|
||||
{editingGroup && (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<h4 style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Share2 size={15} /> {t('vault_share_group_action')}</h4>
|
||||
<div className="text-muted" style={{ fontSize: '.78rem', marginBottom: 8 }}>
|
||||
{t('vault_share_group_hint_prefix')}{editingGroup.entry_count}{t('vault_share_group_hint_suffix')}
|
||||
</div>
|
||||
<Field label={t('vault_share_add')}>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<ComboBox
|
||||
options={(users || []).map((u) => ({ value: u.id, label: u.name }))}
|
||||
value={groupSharingWith}
|
||||
onChange={setGroupSharingWith}
|
||||
placeholder={t('vault_share_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="standard" icon={UserPlus} onClick={shareGroup} disabled={!groupSharingWith}>{t('vault_share_add')}</Button>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
</DetailPopup>
|
||||
|
||||
<DetailPopup open={changingPin} title={t('vault_change_pin')} onClose={() => setChangingPin(false)} width={360} height={360}>
|
||||
|
||||
Reference in New Issue
Block a user