diff --git a/src/ui/components/NodeList/NodeRow.tsx b/src/ui/components/NodeList/NodeRow.tsx
index 46b89e90..e70e256c 100644
--- a/src/ui/components/NodeList/NodeRow.tsx
+++ b/src/ui/components/NodeList/NodeRow.tsx
@@ -9,6 +9,7 @@ import { HtmlText } from "../Shared/HtmlText";
type Props = {
node: PartialNodeInfo;
+ duplicatesCount?: number;
action?: ComponentChildren;
keyComponent?: ComponentChildren;
nsComponent?: ComponentChildren;
@@ -18,6 +19,7 @@ type Props = {
export const NodeRow = ({
node,
+ duplicatesCount,
action,
keyComponent,
nsComponent,
@@ -64,6 +66,9 @@ export const NodeRow = ({
{Object.keys(node.paramsValues ?? {}).length > 0 && (
parameters
)}
+ {typeof duplicatesCount === "number" && duplicatesCount > 1 && (
+ {`${duplicatesCount}x`}
+ )}
)}
diff --git a/src/ui/views/Connect/Connect.tsx b/src/ui/views/Connect/Connect.tsx
index 76f6e44a..33129a57 100644
--- a/src/ui/views/Connect/Connect.tsx
+++ b/src/ui/views/Connect/Connect.tsx
@@ -24,13 +24,16 @@ import { useAllTranslations } from "@/ui/hooks/useAllTranslations";
type Props = RouteParam<"connect">;
-export const Connect = ({ node }: Props) => {
+export const Connect = ({ nodes }: Props) => {
const { setRoute } = useGlobalActions();
const config = useGlobalState((c) => c.config);
const language = useGlobalState((c) => c.config?.language);
- const [search, setSearch] = useState(node.key || node.characters);
+ const primaryNode = nodes[0];
+ const [search, setSearch] = useState(
+ primaryNode?.key || primaryNode?.characters || ""
+ );
const [debouncedSearch] = useDebounce(search, 1000);
@@ -59,6 +62,10 @@ export const Connect = ({ node }: Props) => {
ns: string | undefined,
translation: string | undefined
) => {
+ if (nodes.length === 0) {
+ setRoute("index");
+ return;
+ }
if (
!allTranslationsLoadable.isLoading &&
allTranslationsLoadable.translationsData == null
@@ -72,46 +79,40 @@ export const Connect = ({ node }: Props) => {
if (tolgeeTranslation) {
translation = tolgeeTranslation.translation;
await setNodesDataMutation.mutateAsync({
- nodes: [
- {
- ...node,
- translation: tolgeeTranslation.translation || node.characters,
- isPlural: tolgeeTranslation.keyIsPlural,
- pluralParamValue: tolgeeTranslation.keyPluralArgName,
- key,
- ns: ns || "",
- connected: true,
- },
- ],
+ nodes: nodes.map((node) => ({
+ ...node,
+ translation: tolgeeTranslation.translation || node.characters,
+ isPlural: tolgeeTranslation.keyIsPlural,
+ pluralParamValue: tolgeeTranslation.keyPluralArgName,
+ key,
+ ns: ns || "",
+ connected: true,
+ })),
});
setRoute("index");
return;
}
}
await setNodesDataMutation.mutateAsync({
- nodes: [
- {
- ...node,
- translation: translation || node.characters,
- key,
- ns: ns || "",
- connected: true,
- },
- ],
+ nodes: nodes.map((node) => ({
+ ...node,
+ translation: translation || node.characters,
+ key,
+ ns: ns || "",
+ connected: true,
+ })),
});
setRoute("index");
};
const handleRemoveConnection = async () => {
await setNodesDataMutation.mutateAsync({
- nodes: [
- {
- ...node,
- key: "",
- ns: undefined,
- connected: false,
- },
- ],
+ nodes: nodes.map((node) => ({
+ ...node,
+ key: "",
+ ns: undefined,
+ connected: false,
+ })),
});
setRoute("index");
};
@@ -158,7 +159,7 @@ export const Connect = ({ node }: Props) => {
))}
- {node.connected && (
+ {primaryNode?.connected && (
diff --git a/src/ui/views/Index/Index.tsx b/src/ui/views/Index/Index.tsx
index fd86e9ee..33cd6779 100644
--- a/src/ui/views/Index/Index.tsx
+++ b/src/ui/views/Index/Index.tsx
@@ -24,6 +24,7 @@ import styles from "./Index.css";
import { ListItem } from "./ListItem";
import { useEditorMode } from "../../hooks/useEditorMode";
import { useHasNamespacesEnabled } from "../../hooks/useHasNamespacesEnabled";
+import { NodeInfo } from "@/types";
export const Index = () => {
const selectionLoadable = useSelectedNodes();
@@ -93,6 +94,31 @@ export const Index = () => {
const nothingSelected = !selectionLoadable.data?.somethingSelected;
+ const groupedSelection = useMemo(() => {
+ const groups = new Map();
+ selection.forEach((node) => {
+ const textKey = node.characters.trim();
+ const groupKey = node.connected
+ ? `connected:${node.key}::${node.ns ?? ""}::${textKey}`
+ : `unconnected:${textKey}`;
+ const group = groups.get(groupKey);
+ if (group) {
+ group.push(node);
+ } else {
+ groups.set(groupKey, [node]);
+ }
+ });
+
+ return Array.from(groups.values())
+ .map((nodes, index) => ({
+ id: `group-${index}-${nodes[0]?.id ?? ""}`,
+ key: nodes[0]?.characters.trim() ?? "",
+ nodes,
+ duplicatesCount: nodes.length,
+ }))
+ .sort((a, b) => a.key.localeCompare(b.key));
+ }, [selection]);
+
const handleLanguageChange = (lang: string) => {
if (lang !== language) {
setRoute("pull", { lang });
@@ -102,10 +128,12 @@ export const Index = () => {
const defaultNamespace = useGlobalState((c) => c.config?.namespace);
const handlePush = () => {
- const subjectNodes = selection.map((node) => ({
- ...node,
- ns: node.ns ?? defaultNamespace,
- }));
+ const subjectNodes = groupedSelection
+ .flatMap((group) => group.nodes)
+ .map((node) => ({
+ ...node,
+ ns: node.ns ?? defaultNamespace,
+ }));
const conflicts = getConflictingNodes(subjectNodes);
if (conflicts.length > 0) {
const keys = Array.from(new Set(conflicts.map((n) => n.key)));
@@ -131,7 +159,7 @@ export const Index = () => {
useEffect(() => {
setError(undefined);
- }, [selection]);
+ }, [groupedSelection]);
const editorMode = useEditorMode();
@@ -235,11 +263,13 @@ export const Index = () => {
) : (
(
+ items={groupedSelection}
+ row={(group) => (
({
name,
}))}
diff --git a/src/ui/views/Index/ListItem.tsx b/src/ui/views/Index/ListItem.tsx
index 57985b61..efc1ead6 100644
--- a/src/ui/views/Index/ListItem.tsx
+++ b/src/ui/views/Index/ListItem.tsx
@@ -17,6 +17,8 @@ type UsedNamespaceModel = components["schemas"]["UsedNamespaceModel"];
type Props = {
node: NodeInfo;
+ groupedNodes?: NodeInfo[];
+ duplicatesCount?: number;
loadedNamespaces: UsedNamespaceModel[] | undefined;
hasNamespacesEnabled: boolean;
onRefreshNamespaces?: () => void;
@@ -24,11 +26,20 @@ type Props = {
export const ListItem = ({
node,
+ groupedNodes,
+ duplicatesCount,
loadedNamespaces,
hasNamespacesEnabled,
onRefreshNamespaces,
}: Props) => {
const nodeId = node?.id ?? "";
+ const effectiveNodes = useMemo(
+ () =>
+ groupedNodes && groupedNodes.length > 0
+ ? groupedNodes
+ : [node],
+ [groupedNodes, node]
+ );
const tolgeeConfig = useGlobalState((c) => c.config);
const prefilledKey = usePrefilledKey(
@@ -62,15 +73,26 @@ export const ListItem = ({
// Debounced mutation: only update Figma nodes after user stops typing
useEffect(() => {
- if (!node.connected && debouncedKeyName !== (node.key || "")) {
+ const hasConnected = effectiveNodes.some((current) => current.connected);
+ if (hasConnected) {
+ return;
+ }
+ const shouldUpdate = effectiveNodes.some(
+ (current) => debouncedKeyName !== (current.key || "")
+ );
+ if (shouldUpdate) {
setNodesDataMutation.mutate({
- nodes: [{ ...node, key: debouncedKeyName, ns: namespace }],
+ nodes: effectiveNodes.map((current) => ({
+ ...current,
+ key: debouncedKeyName,
+ ns: namespace,
+ })),
});
}
- }, [debouncedKeyName, namespace, node, node.connected]);
+ }, [debouncedKeyName, namespace, effectiveNodes]);
- const handleConnect = (node: NodeInfo) => {
- setRoute("connect", { node });
+ const handleConnect = () => {
+ setRoute("connect", { nodes: effectiveNodes });
};
const namespaces = useMemo(
@@ -89,25 +111,43 @@ export const ListItem = ({
};
useEffect(() => {
- if (!node.connected && keyName && namespace !== node.ns) {
+ const hasConnected = effectiveNodes.some((current) => current.connected);
+ if (hasConnected || !keyName) {
+ return;
+ }
+ const shouldUpdate = effectiveNodes.some(
+ (current) => current.ns !== namespace
+ );
+ if (shouldUpdate) {
setNodesDataMutation.mutate({
- nodes: [{ ...node, key: keyName, ns: namespace }],
+ nodes: effectiveNodes.map((current) => ({
+ ...current,
+ key: keyName,
+ ns: namespace,
+ })),
});
}
- }, [namespace, node.connected]);
+ }, [namespace, keyName, effectiveNodes]);
- const handleNsChange = (node: NodeInfo) => (value: string) => {
+ const handleNsChange = () => (value: string) => {
setNamespace(value);
setNodesDataMutation.mutate({
- nodes: [{ ...node, key: keyName, ns: value }],
+ nodes: effectiveNodes.map((current) => ({
+ ...current,
+ key: keyName,
+ ns: value,
+ })),
+ });
+ effectiveNodes.forEach((current) => {
+ current.key = keyName;
+ current.ns = value;
});
- node.key = keyName;
- node.ns = value;
};
return (
@@ -119,7 +159,7 @@ export const ListItem = ({
)
@@ -134,7 +174,7 @@ export const ListItem = ({
? "Connect to existing key"
: "Edit key connection"
}
- onClick={() => handleConnect(node)}
+ onClick={handleConnect}
className={styles.connectButton}
>
{node.connected ? (
diff --git a/src/ui/views/routes.ts b/src/ui/views/routes.ts
index 0ef06917..6cf3a416 100644
--- a/src/ui/views/routes.ts
+++ b/src/ui/views/routes.ts
@@ -6,7 +6,7 @@ export type Route =
| ["push"]
| ["string_details", { node: NodeInfo }]
| ["pull", { lang: string }]
- | ["connect", { node: NodeInfo }]
+ | ["connect", { nodes: NodeInfo[] }]
| ["create_copy"];
export type RouteKey = Route[0];