Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/main/endpoints/formatText.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { NodeInfo } from "../../types";
import { createEndpoint } from "../utils/createEndpoint";

// Cached across all formatText calls in a session. listAvailableFontsAsync is
// one of the most expensive Figma API calls; calling it once per node during
// a multi-node pull would multiply the cost unnecessarily.
let availableFontsCache: Font[] | null = null;
// In-flight promise prevents concurrent callers from each issuing the request.
let availableFontsPromise: Promise<Font[]> | null = null;

export type FormatTextEndpointArgs = {
/** The text that is to be displayed in the textNode. Can contain some basic html tags that will be replaced */
formatted: string;
Expand Down Expand Up @@ -64,12 +71,13 @@ export const formatText = async ({
return ranges;
};

let cachedAvailableFonts: Font[] | null = null;
const getAvailableFonts = async () => {
if (!cachedAvailableFonts) {
cachedAvailableFonts = await figma.listAvailableFontsAsync();
if (availableFontsCache) return availableFontsCache;
if (!availableFontsPromise) {
availableFontsPromise = figma.listAvailableFontsAsync();
}
return cachedAvailableFonts;
availableFontsCache = await availableFontsPromise;
return availableFontsCache;
};

const getFontsOfFamily = async (font: FontName) =>
Expand Down
31 changes: 25 additions & 6 deletions src/main/endpoints/getConnectedNodes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TOLGEE_NODE_INFO } from "@/constants";
import { NodeInfo } from "@/types";
import { createEndpoint } from "../utils/createEndpoint";
import { findTextNodesInfo } from "../utils/nodeTools";
import { findTextNodesInfo, getNodeInfo } from "../utils/nodeTools";

export type ConnectedNodesProps = {
ignoreSelection: boolean;
Expand All @@ -12,11 +13,29 @@ export const getConnectedNodesEndpoint = createEndpoint<
>("GET_CONNECTED_NODES", async ({ ignoreSelection }) => {
const basedOnSelection =
!ignoreSelection && figma.currentPage.selection.length > 0;
const items = basedOnSelection
? figma.currentPage.selection
: figma.currentPage.children;

if (basedOnSelection) {
// Selection path: still walk manually since selection may be a subtree of
// any depth and `findAllWithCriteria` only operates on the whole page.
return {
items: findTextNodesInfo(figma.currentPage.selection).filter(
({ key }) => key,
),
basedOnSelection: true,
};
}

// Page-wide path: let the Figma runtime filter by plugin-data key natively.
// This avoids recursing through every node in JS and calling getPluginData
// on every TextNode — O(all nodes) → O(connected nodes).
await figma.currentPage.loadAsync();
const connected = figma.currentPage.findAllWithCriteria({
types: ["TEXT"],
pluginData: { keys: [TOLGEE_NODE_INFO] },
}) as TextNode[];

return {
items: findTextNodesInfo(items).filter(({ key }) => key),
basedOnSelection,
items: connected.map(getNodeInfo),
basedOnSelection: false,
};
});
19 changes: 13 additions & 6 deletions src/main/utils/settingsTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ import {
import { emit } from "@create-figma-plugin/utilities";
import { setPageData } from "./pages";

const getGlobalSettings = async () => {
const getGlobalSettings = async (): Promise<Partial<GlobalSettings>> => {
const pluginData = await figma.clientStorage.getAsync(
TOLGEE_PLUGIN_CONFIG_NAME,
);
return pluginData ? (JSON.parse(pluginData) as Partial<GlobalSettings>) : {};
if (!pluginData) return {};
// clientStorage may return an already-parsed object (Figma auto-parses JSON
// strings, or old data was stored without JSON.stringify). Handle both.
if (typeof pluginData === "string") {
try {
return JSON.parse(pluginData) as Partial<GlobalSettings>;
} catch {
return {};
}
}
return pluginData as Partial<GlobalSettings>;
};

const setGlobalSettings = async (data: Partial<GlobalSettings>) => {
await figma.clientStorage.setAsync(
TOLGEE_PLUGIN_CONFIG_NAME,
JSON.stringify(data),
);
await figma.clientStorage.setAsync(TOLGEE_PLUGIN_CONFIG_NAME, data);
};

export const deleteGlobalSettings = async () => {
Expand Down
8 changes: 7 additions & 1 deletion src/ui/hooks/useConnectedNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export const useConnectedNodes = (props: ConnectedNodesProps) => {
return useQuery(
queryKey,
delayed(() => getConnectedNodesEndpoint.call(props)),
{ select: (data) => ({ ...data, items: data.items.filter((n) => n.key) }) },
{
select: (data) => ({ ...data, items: data.items.filter((n) => n.key) }),
// Keeps data "fresh" long enough that an optimistic cache patch written
// by useSetNodesDataMutation.onSuccess prevents an unnecessary full-page
// rescan when returning to Index right after a connect/disconnect.
staleTime: 30_000,
},
);
};
43 changes: 36 additions & 7 deletions src/ui/hooks/useSetNodesDataMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,51 @@ import {
setNodesDataEndpoint,
} from "@/main/endpoints/setNodesData";
import { getConnectedNodesEndpoint } from "@/main/endpoints/getConnectedNodes";
import { getSelectedNodesEndpoint } from "@/main/endpoints/getSelectedNodes";
import { delayed } from "@/main/utils/delayed";
import { NodeInfo } from "@/types";
import { useMutation, useQueryClient } from "react-query";

type CachedNodes = { items: NodeInfo[]; basedOnSelection: boolean };

export const useSetNodesDataMutation = () => {
const queryClient = useQueryClient();
const result = useMutation<void, unknown, SetNodesDataProps>(
[setNodesDataEndpoint.name],
delayed((props: SetNodesDataProps) => setNodesDataEndpoint.call(props)),
{
onSuccess: () => {
// Mark connected-nodes data stale without triggering an immediate
// refetch. Refetching on every keystroke walks the entire page tree
// (see getConnectedNodes with ignoreSelection: true) and froze the UI
// while typing. Stale data is refetched on the next mount, e.g. when
// navigating back to Index/Pull/Push after Connect.
queryClient.invalidateQueries([getConnectedNodesEndpoint.name], {
onSuccess: (_, { nodes }) => {
// Patch the page-wide cache directly so returning to Index does not
// trigger a full-page rescan. useConnectedNodes uses staleTime:30s,
// so fresh data here prevents the redundant allNodes.refetch() call.
const key = [getConnectedNodesEndpoint.name, true] as const;
const old = queryClient.getQueryData<CachedNodes>(key);
if (old) {
const patch = new Map(nodes.map((n) => [n.id, n]));
const updated = old.items
.map((item) =>
patch.has(item.id) ? { ...item, ...patch.get(item.id)! } : item,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.filter((item) => item.key && item.connected !== false);
// Add newly connected nodes that were not tracked before
for (const n of nodes) {
if (
n.key &&
n.connected !== false &&
!old.items.some((i) => i.id === n.id)
) {
updated.push(n);
}
}
queryClient.setQueryData<CachedNodes>(key, {
...old,
items: updated,
});
}
// Mark selection stale so Index re-fetches node characters after
// navigating back from Pull (Index remounts, mountedRef resets to
// false, so the route-change effect never fires on that remount).
queryClient.invalidateQueries([getSelectedNodesEndpoint.name], {
refetchActive: false,
refetchInactive: false,
});
Expand Down
2 changes: 1 addition & 1 deletion src/ui/views/Connect/Connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const Connect = ({ node }: Props) => {
nodes: [
{
...node,
translation: resolvedTranslation || node.characters,
translation: resolvedTranslation ?? node.characters,
isPlural: isPlural ?? node.isPlural,
pluralParamValue: pluralParamValue ?? node.pluralParamValue,
key,
Expand Down
6 changes: 5 additions & 1 deletion src/ui/views/Index/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ export const Index = () => {
}
if (route[0] === "index") {
selectionLoadable.refetch();
allNodes.refetch();
// Skip the page-wide rescan when the mutation already patched the cache
// (useSetNodesDataMutation.onSuccess + staleTime:30s keeps it fresh).
if (allNodes.isStale) {
allNodes.refetch();
}
}
}, [route]);

Expand Down
Loading