From 0f9d32e1f59593e85eee129613be35b36aeacf26 Mon Sep 17 00:00:00 2001 From: onenewcode Date: Wed, 26 Aug 2026 14:44:19 +0800 Subject: [PATCH] feat(redis): edit hash fields --- .../src/components/redis/RedisValueViewer.vue | 51 +++- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/ja.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + .../redis/RedisValueViewer.redisJson.spec.ts | 23 +- apps/desktop/src/lib/backend/api.ts | 1 + apps/desktop/src/lib/backend/http.ts | 11 + apps/desktop/src/lib/backend/tauri.ts | 11 + crates/dbx-core/src/db/redis_driver.rs | 288 ++++++++++++++++++ crates/dbx-core/src/redis_ops.rs | 37 +++ crates/dbx-web/src/main.rs | 1 + crates/dbx-web/src/routes/redis.rs | 30 ++ src-tauri/src/commands/redis_cmd.rs | 23 ++ src-tauri/src/lib.rs | 1 + 18 files changed, 470 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/components/redis/RedisValueViewer.vue b/apps/desktop/src/components/redis/RedisValueViewer.vue index 629860b86b..ea973be330 100644 --- a/apps/desktop/src/components/redis/RedisValueViewer.vue +++ b/apps/desktop/src/components/redis/RedisValueViewer.vue @@ -173,6 +173,10 @@ const selectedMemberContext = ref(null); const isEditingMember = ref(false); const savingMember = ref(false); const memberEditValue = ref(""); +const memberFieldEditValue = ref(""); +// Keep the original hash field separate from the editable draft so a rename +// remains visible (and protected from refresh) after the detail dialog closes. +const memberFieldDraftBaseline = ref(""); const editingZsetMemberKey = ref(null); const savingZsetMember = ref(false); const zsetInlineMember = ref(""); @@ -590,17 +594,22 @@ const hasRetainedStringDraft = computed(() => isStringDraftDirty(stringDraftForm const redisJsonValueChanged = computed(() => data.value?.data.kind === "json" && editValue.value !== redisJsonDraftBaseline.value); const canEditCurrentMemberFormat = computed(() => selectedMemberCanEdit.value && memberValueView.value === "utf8"); const isEditingHashJson = computed(() => selectedMemberContext.value?.kind === "hash" && selectedMemberCanEdit.value && memberValueView.value === "json" && Boolean(selectedMemberDetail.value.json)); +const memberFieldChanged = computed(() => { + const context = selectedMemberContext.value; + return context?.kind === "hash" && context.canEdit && memberFieldEditValue.value !== memberFieldDraftBaseline.value; +}); const memberValueChanged = computed(() => { if (!selectedMemberCanEdit.value) return false; const original = selectedMemberDetail.value.rawText; - if (isEditingHashJson.value && selectedMemberDetail.value.json) return memberEditValue.value !== memberJsonDraftBaseline.value; - return memberEditValue.value !== original; + const valueChanged = isEditingHashJson.value && selectedMemberDetail.value.json ? memberEditValue.value !== memberJsonDraftBaseline.value : memberEditValue.value !== original; + return valueChanged || memberFieldChanged.value; }); // A member sheet can close without discarding its draft, so its dirty state // must outlive the sheet and whichever display format is currently selected. const memberDraftFormat = ref<"utf8" | "json" | null>(null); const hasRetainedMemberDraft = computed(() => { const format = memberDraftFormat.value; + if (memberFieldChanged.value) return true; if (!format || !selectedMemberCanEdit.value) return false; const original = selectedMemberDetail.value.rawText; @@ -609,7 +618,7 @@ const hasRetainedMemberDraft = computed(() => { } return memberEditValue.value !== original; }); -const hasUnsavedRedisDraft = computed(() => hasRetainedStringDraft.value || redisJsonValueChanged.value || hasRetainedMemberDraft.value || editingZsetMemberKey.value !== null || showHashFieldTtlDialog.value); +const hasUnsavedRedisDraft = computed(() => hasRetainedStringDraft.value || redisJsonValueChanged.value || hasRetainedMemberDraft.value || memberFieldChanged.value || editingZsetMemberKey.value !== null || showHashFieldTtlDialog.value); const hasMore = computed(() => scanCursor.value != null && scanCursor.value > 0); const collectionTotal = computed(() => (data.value ? redisValueCollectionTotal(data.value) : null)); const hashFieldTtlSupported = computed(() => { @@ -1824,6 +1833,8 @@ function selectMember(title: string, value: unknown, context: RedisMemberContext memberJsonRawBaseline.value = detail.json?.formattedText ?? ""; memberJsonDraftBaseline.value = jsonDraftBaseline(memberJsonRawBaseline.value, redisJsonDecoded.value); memberEditValue.value = memberValueView.value === "json" && detail.json ? memberJsonDraftBaseline.value : detail.rawText; + memberFieldDraftBaseline.value = context.kind === "hash" ? (context.field ?? "") : ""; + memberFieldEditValue.value = memberFieldDraftBaseline.value; memberDraftFormat.value = context.kind === "hash" && context.canEdit && memberValueView.value === "json" && detail.json ? "json" : null; } @@ -1834,6 +1845,8 @@ function clearSelectedMember() { selectedMemberContext.value = null; isEditingMember.value = false; memberEditValue.value = ""; + memberFieldEditValue.value = ""; + memberFieldDraftBaseline.value = ""; memberJsonDraftBaseline.value = ""; memberDraftFormat.value = null; } @@ -1857,7 +1870,9 @@ function viewMember(title: string, value: unknown, context: RedisMemberContext, function handleMemberDetailOpenChange(open: boolean) { showMemberDetail.value = open; - if (!open) isEditingMember.value = false; + if (!open) { + isEditingMember.value = false; + } } function finishMemberDetailClose() { @@ -1988,6 +2003,11 @@ function startEditMember() { if (!memberValueChanged.value) memberEditValue.value = selectedMemberDetail.value.rawText; // Do not demote a retained JSON draft to utf8; save still needs compact normalization. if (memberDraftFormat.value !== "json") memberDraftFormat.value = "utf8"; + // A closed dialog can be reopened into the same edit. Keep a dirty field + // draft instead of replacing it with the currently stored field name. + if (selectedMemberContext.value?.kind === "hash" && !memberFieldChanged.value) { + memberFieldEditValue.value = selectedMemberContext.value.field ?? ""; + } isEditingMember.value = true; nextTick(() => memberTextareaRef.value?.focus()); } @@ -1996,6 +2016,7 @@ function cancelEditMember() { memberEditValue.value = selectedMemberDetail.value.rawText; memberDraftFormat.value = null; isEditingMember.value = false; + memberFieldEditValue.value = memberFieldDraftBaseline.value; } function discardHashJsonEdit() { @@ -2020,13 +2041,24 @@ async function saveMemberEdit() { } let nextContext: RedisMemberContext = context; + const nextHashField = context.kind === "hash" ? memberFieldEditValue.value : ""; + if (context.kind === "hash" && nextHashField.length === 0) { + toast(t("redis.fieldRequired"), 3000); + return; + } savingMember.value = true; try { if (context.kind === "list") { await api.redisListSet(props.connectionId, props.db, props.keyRaw, context.index, writeValue); } else if (context.kind === "hash") { - if (!context.field) return; - await api.redisHashSet(props.connectionId, props.db, props.keyRaw, context.field, writeValue); + if (context.field == null) return; + try { + await api.redisHashFieldUpdate(props.connectionId, props.db, props.keyRaw, context.field, nextHashField, writeValue); + } catch (error) { + toast(errorMessage(error), 3000); + return; + } + nextContext = { kind: "hash", field: nextHashField, canEdit: canEditRedisMemberDetail("hash", writeValue) }; } else if (context.kind === "set") { if (!context.member) return; await api.redisSetRemove(props.connectionId, props.db, props.keyRaw, context.member); @@ -2149,7 +2181,7 @@ function resolveSelectedMember(context: RedisMemberContext): { title: string; va }; } case "hash": { - if (redisKind.value !== "hash" || !context.field) return null; + if (redisKind.value !== "hash" || context.field == null) return null; const item = (collectionItems.value as RedisHashItem[]).find((candidate) => redisBlobText(candidate.field) === context.field); if (!item) return null; const field = redisBlobText(item.field); @@ -3480,7 +3512,8 @@ defineExpose({ focusSearch }); /> - {{ selectedMemberTitle ? formatValue(selectedMemberTitle) : t("redis.memberDetail") }} + + {{ selectedMemberTitle ? formatValue(selectedMemberTitle) : t("redis.memberDetail") }} {{ redisFormatLabel(memberValueView, selectedMemberDetail.rawLabel) }} @@ -3596,7 +3629,7 @@ defineExpose({ focusSearch }); -