mod-arch-shared
Version:
Shared UI components and utilities for modular architecture micro-frontend projects
133 lines • 7.23 kB
JavaScript
import React, { useState } from 'react';
import { Label, LabelGroup, Alert, AlertVariant } from '@patternfly/react-core';
import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing';
import DashboardDescriptionListGroup from '../components/DashboardDescriptionListGroup';
export const EditableLabelsDescriptionListGroup = ({ title = 'Labels', contentWhenEmpty = 'No labels', labels, onLabelsChange, isArchive, allExistingKeys, labelProps = {}, isCollapsible = true, overflowCount, onEditingChange, }) => {
const [isSavingEdits, setIsSavingEdits] = useState(false);
const [hasSavedEdits, setHasSavedEdits] = useState(false);
const [unsavedLabels, setUnsavedLabels] = useState(labels);
const [isEditing, setIsEditing] = React.useState(false);
const handleEditingStateChange = (editingState) => {
setIsEditing(editingState);
onEditingChange?.(editingState);
};
const validateLabels = () => {
const errors = [];
const duplicatesMap = new Map();
unsavedLabels.forEach((label) => {
duplicatesMap.set(label, (duplicatesMap.get(label) || 0) + 1);
});
const duplicateLabels = [];
duplicatesMap.forEach((count, label) => {
if (count > 1) {
duplicateLabels.push(label);
}
});
if (duplicateLabels.length > 0) {
if (duplicateLabels.length === 1) {
const label = duplicateLabels[0] ?? '';
errors.push(`**${label}** already exists as a label. Ensure that each label is unique.`);
}
else {
const lastLabel = duplicateLabels.pop() ?? '';
const formattedLabels = duplicateLabels.map((label) => `**${label}**`).join(', ');
errors.push(`${formattedLabels} and **${lastLabel}** already exist as labels. Ensure that each label is unique.`);
}
}
unsavedLabels.forEach((label) => {
if (allExistingKeys.includes(label) && !labels.includes(label)) {
errors.push(`**${label}** already exists as a property key. Labels cannot use the same name as existing properties.`);
}
if (label.length > 63) {
errors.push(`**${label}** can't exceed 63 characters`);
}
});
return errors;
};
const handleEditComplete = (_event, newText, currentLabel) => {
if (!newText) {
return;
}
setUnsavedLabels((prev) => {
if (currentLabel) {
const index = prev.indexOf(currentLabel);
if (index === -1) {
return [...prev, newText];
}
const newLabels = [...prev];
newLabels[index] = newText;
return newLabels;
}
return [...prev, newText];
});
};
const removeUnsavedLabel = (index) => {
if (isSavingEdits) {
return;
}
setUnsavedLabels(unsavedLabels.filter((_, i) => i !== index));
};
const addNewLabel = () => {
if (isSavingEdits) {
return;
}
const baseLabel = 'New Label';
let counter = 1;
let newLabel = baseLabel;
while (unsavedLabels.includes(newLabel)) {
newLabel = `${baseLabel} ${counter}`;
counter++;
}
setUnsavedLabels((prev) => {
const updated = [...prev, newLabel];
return updated;
});
};
const labelErrors = validateLabels();
const hasDuplicate = (label, index) => {
const firstIndex = unsavedLabels.findIndex((l) => l === label);
if (firstIndex !== index) {
return true;
}
if (allExistingKeys.includes(label) && !labels.includes(label)) {
return true;
}
return false;
};
return (React.createElement(DashboardDescriptionListGroup, { editButtonTestId: "editable-labels-group-edit", saveButtonTestId: "editable-labels-group-save", title: title, isEmpty: labels.length === 0, contentWhenEmpty: contentWhenEmpty, isEditable: !isArchive, isEditing: isEditing, isSavingEdits: isSavingEdits, contentWhenEditing: React.createElement(React.Fragment, null,
React.createElement(LabelGroup, { "data-testid": "editable-label-group", isEditable: !isSavingEdits, numLabels: unsavedLabels.length, addLabelControl: !isSavingEdits ? (React.createElement(Label, { variant: "overflow", color: "blue", onClick: addNewLabel, "data-testid": "add-label-button" }, "Add label")) : undefined, className: spacing.mbMd }, unsavedLabels.map((label, index) => (React.createElement(Label, { "data-testid": `editable-label-${label}`, key: label + index, isEditable: !isSavingEdits, onClose: () => removeUnsavedLabel(index), closeBtnProps: {
isDisabled: isSavingEdits,
'data-testid': `remove-label-${label}`,
}, onEditComplete: (event, newText) => handleEditComplete(event, newText, label), editableProps: {
defaultValue: label,
'aria-label': 'Edit label',
'data-testid': `edit-label-input-${label}`,
}, ...labelProps, color: hasDuplicate(label, index) ? 'red' : (labelProps.color ?? 'blue'), variant: hasDuplicate(label, index) ? 'filled' : labelProps.variant }, label)))),
labelErrors.length > 0 &&
labelErrors.map((error, index) => (React.createElement(Alert, { key: index, "data-testid": "label-error-alert", variant: AlertVariant.danger, isInline: true, title: error
.split('**')
.map((part, i) => (i % 2 === 0 ? part : React.createElement("strong", { key: i }, part))), "aria-live": "polite", isPlain: true, tabIndex: -1 }))),
onEditingChange && isEditing && (React.createElement(Alert, { "data-testid": "editing-labels-alert", variant: AlertVariant.info, isInline: true, title: "Changes affect all model versions", "aria-live": "polite", isPlain: true, tabIndex: -1 }))), onEditClick: () => {
setUnsavedLabels(labels);
handleEditingStateChange(true);
}, onSaveEditsClick: async () => {
if (labelErrors.length > 0) {
return;
}
setIsSavingEdits(true);
try {
await onLabelsChange(unsavedLabels);
}
finally {
setHasSavedEdits(true);
setIsSavingEdits(false);
handleEditingStateChange(false);
}
}, onDiscardEditsClick: () => {
setUnsavedLabels(labels);
handleEditingStateChange(false);
} },
React.createElement(LabelGroup, { "data-testid": "display-label-group", defaultIsOpen: hasSavedEdits, key: String(hasSavedEdits), numLabels: isCollapsible ? overflowCount : labels.length }, labels.map((label) => (React.createElement(Label, { key: label, color: "blue", "data-testid": "label", ...labelProps }, label))))));
};
export default EditableLabelsDescriptionListGroup;
//# sourceMappingURL=EditableLabelsDescriptionListGroup.js.map