feat(furni-editor): sync empty public_name from furnidata name

When a furni has a matching furnidata entry with a display name but its
items_base.public_name (the DB fallback) is empty, the editor now shows a
'Sync from furnidata' button next to the Public Name field. It reuses the
generic item update (a partial { publicName } payload) to fill the DB column
from the stored furnidata name, so the read-only fallback stops being blank.
Button shows only when the entry's classname matches, the DB field is empty,
and the furnidata name is present; it disappears after the sync re-fetch.
This commit is contained in:
simoleo89
2026-06-14 16:41:55 +02:00
parent 88d2128295
commit b27f48f2a2
3 changed files with 29 additions and 3 deletions
@@ -19,7 +19,7 @@ export const FurniEditorView: FC<{}> = () =>
selectedItem, setSelectedItem, furniDataEntry, furniDataDiagnostic,
interactions,
searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions,
updateFurnidata, revertFurnidata, importText, importResult
updateFurnidata, revertFurnidata, syncPublicName, importText, importResult
} = useFurniEditor();
const isMod = useHasPermission('acc_catalogfurni');
@@ -159,6 +159,7 @@ export const FurniEditorView: FC<{}> = () =>
onBack={ handleBack }
onUpdateFurnidata={ updateFurnidata }
onRevertFurnidata={ revertFurnidata }
onSyncPublicName={ syncPublicName }
onImportText={ importText }
importResult={ importResult }
/>
@@ -15,6 +15,7 @@ interface FurniEditorEditViewProps
onBack: () => void;
onUpdateFurnidata: (id: number, name: string, description: string) => void;
onRevertFurnidata: (id: number) => void;
onSyncPublicName: (id: number, name: string) => void;
onImportText: (id: number) => void;
importResult: { found: boolean; name: string; description: string; classname: string; nonce: number } | null;
}
@@ -122,7 +123,7 @@ const CopyValue: FC<{ value: string | number }> = ({ value }) =>
export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
{
const { item, furniDataEntry, furniDataDiagnostic, interactions, loading, onUpdate, onDelete, onBack, onUpdateFurnidata, onRevertFurnidata, onImportText, importResult } = props;
const { item, furniDataEntry, furniDataDiagnostic, interactions, loading, onUpdate, onDelete, onBack, onUpdateFurnidata, onRevertFurnidata, onSyncPublicName, onImportText, importResult } = props;
const saveRef = useRef<() => void>(null);
const [ form, setForm ] = useState({
@@ -251,6 +252,15 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
// classname), which stays locked to avoid an id collision.
const furnidataCreatable = useMemo(() => !furniDataEntry, [ furniDataEntry ]);
// Show a one-click "sync" when the DB public_name is empty but the (matching)
// furnidata entry already has a name — fills items_base.public_name from the
// stored furnidata name so the DB fallback stops being blank.
const canSyncPublicName = useMemo(() =>
furnidataEditable &&
!String(form.publicName ?? '').trim() &&
!!String(furniDataEntry?.name ?? '').trim(),
[ furnidataEditable, form.publicName, furniDataEntry ]);
// True only when the name/description actually differ from the stored furnidata
// entry. Used to gate the Save button: saving an unchanged value makes the
// server writer return false, which the handler misreports as "Classname not
@@ -427,6 +437,10 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
<div>
<label className={ labelClass }>Public Name (DB fallback)</label>
<CopyValue value={ form.publicName } />
{ canSyncPublicName &&
<Button variant="secondary" disabled={ loading } className="mt-1 w-full" onClick={ () => onSyncPublicName(item.id, String(furniDataEntry?.name ?? '')) }>
Sync from furnidata
</Button> }
</div>
<div>
<label className={ labelClass }>Sprite ID</label>
+12 -1
View File
@@ -291,6 +291,17 @@ export const useFurniEditor = () =>
SendMessageComposer(new FurniEditorRevertFurnidataComposer(id));
}, []);
// Fill an empty items_base.public_name from the furnidata display name. Reuses
// the generic item update (a partial { publicName } payload is accepted), so the
// existing 'update' result path shows the toast and re-fetches the detail.
const syncPublicName = useCallback((id: number, name: string) =>
{
setLoading(true);
setError(null);
pendingActionRef.current = { action: 'update', itemId: id };
SendMessageComposer(new FurniEditorUpdateComposer(id, JSON.stringify({ publicName: name })));
}, []);
const importText = useCallback((id: number) =>
{
setLoading(true);
@@ -323,6 +334,6 @@ export const useFurniEditor = () =>
selectedItem, setSelectedItem, catalogItems, furniDataEntry, furniDataDiagnostic,
interactions,
searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions,
updateFurnidata, revertFurnidata, importText, importResult
updateFurnidata, revertFurnidata, syncPublicName, importText, importResult
};
};