221 lines
7.5 KiB
JavaScript
221 lines
7.5 KiB
JavaScript
/*
|
|
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* Metadata Editor Core - Business Logic Only
|
|
*
|
|
* This module contains pure data operations for metadata management.
|
|
* It does NOT import from any controllers, following the dependency rule:
|
|
* controllers → cores → stores → utils
|
|
*
|
|
* UI code (dialogs, forms) has been moved to metadataDialogController.js
|
|
*/
|
|
|
|
import { METADATA_FIELD_CONFIG, REMIX_KEYS, REMIX_TYPE, EVENTS } from "../utils/constants.js";
|
|
import { buildMetadataContext, getApplicableMetadataFields } from "./workflowExportCore.js";
|
|
|
|
/**
|
|
* Get all unique group names from the current workflow.
|
|
* Scans all nodes with tagged inputs and collects their group names.
|
|
* @param {Object} app - ComfyUI app instance
|
|
* @returns {string[]} Sorted array of unique group names
|
|
*/
|
|
export function getExistingGroups(app) {
|
|
const groups = new Set();
|
|
const graphNodes = app.graph._nodes || app.graph.nodes || [];
|
|
for (const node of graphNodes) {
|
|
const markedInputs = node.properties?.[REMIX_KEYS.ROOT]?.[REMIX_KEYS.STRUCTURE.INPUTS];
|
|
if (!markedInputs) continue;
|
|
for (const slotName of Object.keys(markedInputs)) {
|
|
const meta = markedInputs[slotName];
|
|
const group = meta?.[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA_ROOT]?.[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA.GROUP];
|
|
if (group) groups.add(group);
|
|
}
|
|
}
|
|
return Array.from(groups).sort();
|
|
}
|
|
|
|
/**
|
|
* Get applicable metadata fields for an input based on its context.
|
|
* Combines context building with field filtering for convenience.
|
|
*
|
|
* @param {Object} app - ComfyUI app instance
|
|
* @param {string|number} nodeId - Node ID
|
|
* @param {string} slotName - Slot name
|
|
* @returns {Object[]} Array of applicable field configs from METADATA_FIELD_CONFIG
|
|
*/
|
|
export function getApplicableMetadataFieldsForInput(app, nodeId, slotName) {
|
|
const node = app.graph.getNodeById?.(nodeId);
|
|
if (!node) return [];
|
|
|
|
const inputMeta = node.properties?.[REMIX_KEYS.ROOT]?.[REMIX_KEYS.STRUCTURE.INPUTS]?.[slotName];
|
|
if (!inputMeta) return [];
|
|
|
|
const context = buildMetadataContext({
|
|
app,
|
|
node,
|
|
slotName,
|
|
primitiveType: inputMeta.type,
|
|
isInput: true,
|
|
remixType: inputMeta.remix_type,
|
|
});
|
|
|
|
return getApplicableMetadataFields(context);
|
|
}
|
|
|
|
/**
|
|
* Get current metadata values for a tagged input.
|
|
*
|
|
* @param {Object} app - ComfyUI app instance
|
|
* @param {string|number} nodeId - Node ID
|
|
* @param {string} slotName - Slot name
|
|
* @returns {Object|null} Metadata values or null if not found
|
|
*/
|
|
export function getInputMetadata(app, nodeId, slotName) {
|
|
const node = app.graph.getNodeById?.(nodeId);
|
|
if (!node) return null;
|
|
|
|
const inputMeta = node.properties?.[REMIX_KEYS.ROOT]?.[REMIX_KEYS.STRUCTURE.INPUTS]?.[slotName];
|
|
if (!inputMeta) return null;
|
|
|
|
return {
|
|
name: inputMeta.name || slotName,
|
|
type: inputMeta.type || "unknown",
|
|
remixType: inputMeta.remix_type || REMIX_TYPE.AUTO,
|
|
order: inputMeta[REMIX_KEYS.PROPERTY.ORDER],
|
|
additionalData: inputMeta[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA_ROOT] || {},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Update metadata for a tagged input.
|
|
*
|
|
* @param {Object} app - ComfyUI app instance
|
|
* @param {string|number} nodeId - Node ID
|
|
* @param {string} slotName - Slot name
|
|
* @param {Object} updates - Fields to update {name?, remixType?, additionalData?}
|
|
* @returns {boolean} True if update was successful
|
|
*/
|
|
export function updateInputMetadata(app, nodeId, slotName, updates) {
|
|
const node = app.graph.getNodeById?.(nodeId);
|
|
if (!node) return false;
|
|
|
|
const inputMeta = node.properties?.[REMIX_KEYS.ROOT]?.[REMIX_KEYS.STRUCTURE.INPUTS]?.[slotName];
|
|
if (!inputMeta) return false;
|
|
|
|
// Update base fields
|
|
if (updates.name !== undefined) {
|
|
inputMeta.name = updates.name;
|
|
}
|
|
if (updates.remixType !== undefined) {
|
|
inputMeta.remix_type = updates.remixType;
|
|
}
|
|
if (updates.order !== undefined) {
|
|
// Order is stored at the property level, not inside additional_data
|
|
inputMeta[REMIX_KEYS.PROPERTY.ORDER] = updates.order;
|
|
}
|
|
|
|
// Update additional data fields
|
|
if (updates.additionalData !== undefined) {
|
|
// Ensure additionalData root exists
|
|
if (!inputMeta[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA_ROOT]) {
|
|
inputMeta[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA_ROOT] = {};
|
|
}
|
|
|
|
const additionalDataRoot = inputMeta[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA_ROOT];
|
|
|
|
// Process each field from METADATA_FIELD_CONFIG
|
|
for (const fieldConfig of METADATA_FIELD_CONFIG) {
|
|
const key = fieldConfig.key;
|
|
const remixKey = REMIX_KEYS.PROPERTY.ADDITIONAL_DATA[key.toUpperCase()];
|
|
if (!remixKey) continue;
|
|
|
|
const newValue = updates.additionalData[key];
|
|
|
|
// If value is empty/null/undefined, DELETE the field from metadata
|
|
if (newValue === null || newValue === undefined || newValue === "") {
|
|
delete additionalDataRoot[remixKey];
|
|
} else {
|
|
// Set the value
|
|
additionalDataRoot[remixKey] = newValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up empty additionalData object
|
|
const additionalData = inputMeta[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA_ROOT];
|
|
if (additionalData && Object.keys(additionalData).length === 0) {
|
|
delete inputMeta[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA_ROOT];
|
|
}
|
|
|
|
app.graph.setDirtyCanvas?.(true, true);
|
|
app.graph.change?.();
|
|
|
|
// Dispatch event for UI updates
|
|
app.api.dispatchEvent(
|
|
new CustomEvent(EVENTS.METADATA_CHANGED, {
|
|
detail: { nodeId, slotName, updates },
|
|
})
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Reassign all inputs from one group to another.
|
|
* Used when renaming or deleting a group.
|
|
* @param {Object} app - ComfyUI app instance
|
|
* @param {string} fromGroup - Group to reassign from
|
|
* @param {string|null} toGroup - Group to reassign to (null/empty = Ungrouped, DELETES metadata)
|
|
*/
|
|
export function reassignGroupInputs(app, fromGroup, toGroup) {
|
|
const graphNodes = app.graph._nodes || app.graph.nodes || [];
|
|
let changed = false;
|
|
|
|
for (const node of graphNodes) {
|
|
const markedInputs = node.properties?.[REMIX_KEYS.ROOT]?.[REMIX_KEYS.STRUCTURE.INPUTS];
|
|
if (!markedInputs) continue;
|
|
|
|
for (const slotName of Object.keys(markedInputs)) {
|
|
const meta = markedInputs[slotName];
|
|
const additionalData = meta?.[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA_ROOT];
|
|
const currentGroup = additionalData?.[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA.GROUP];
|
|
|
|
if (currentGroup === fromGroup) {
|
|
if (!toGroup) {
|
|
// Moving to Ungrouped = DELETE the group metadata entirely
|
|
if (additionalData) {
|
|
delete additionalData[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA.GROUP];
|
|
}
|
|
} else {
|
|
// Rename to new group
|
|
if (!meta[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA_ROOT]) {
|
|
meta[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA_ROOT] = {};
|
|
}
|
|
meta[REMIX_KEYS.PROPERTY.ADDITIONAL_DATA_ROOT][REMIX_KEYS.PROPERTY.ADDITIONAL_DATA.GROUP] = toGroup;
|
|
}
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
app.graph.setDirtyCanvas?.(true, true);
|
|
app.graph.change?.();
|
|
}
|
|
}
|