Skip to content
Closed
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
29 changes: 29 additions & 0 deletions apps/mesh/src/web/components/sandbox/content/app-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,33 @@ describe("app-catalog", () => {
app: "vtex",
});
});

it("lists installed custom/local apps without manifest or store entries", () => {
const emptyMeta: LiveMeta = {
manifest: { blocks: { apps: { anyOf: [] } } },
schema: {},
};
const decofile = {
"app-tags": {
__resolveType: "site/apps/local/app-tags.ts",
account: "lojabagaggio",
},
};

const catalog = buildAppCatalog([], emptyMeta, decofile);

expect(catalog).toEqual([
{
id: "local-app-tags",
app: "app-tags",
vendor: "local",
title: "App Tags",
description: "",
category: "Custom",
resolveType: "site/apps/local/app-tags.ts",
blockKey: "app-tags",
installed: true,
},
]);
});
});
124 changes: 57 additions & 67 deletions apps/mesh/src/web/components/sandbox/content/app-catalog.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {
isSiteAppBlock,
SITE_APP_RESOLVE_TYPE,
type AppEntry,
appLabel,
} from "@/web/components/sections-editor/page-list";
import {
isDecoAppResolveType,
resolveBlockSchemaMetadata,
type LiveMeta,
} from "@/web/components/sections-editor/resolve-schema";
Expand Down Expand Up @@ -55,6 +56,18 @@ export function parseAppResolveType(
return null;
}

function parseAppIdentityFromBlockKey(
blockKey: string,
): { vendor: string; app: string } | null {
const blockIdMatch = blockKey.match(/^([^-]+)-(.+)$/);
if (!blockIdMatch) return null;
return { vendor: blockIdMatch[1]!, app: blockIdMatch[2]! };
}

function installedAppCategory(vendor: string): string {
return vendor === "local" ? "Custom" : "Installed";
}

function findInstalledBlockKey(
vendor: string,
app: string,
Expand Down Expand Up @@ -126,6 +139,37 @@ function catalogEntryFromManifestApp(
};
}

function catalogEntryFromInstalledBlock(
blockKey: string,
block: Record<string, unknown>,
meta: LiveMeta,
): AppCatalogEntry | null {
const resolveType = block.__resolveType;
if (typeof resolveType !== "string") return null;
if (isSiteAppBlock(blockKey, block)) return null;
if (!isDecoAppResolveType(resolveType)) return null;

const parsed =
parseAppResolveType(resolveType) ?? parseAppIdentityFromBlockKey(blockKey);
if (!parsed) return null;

const metadata = resolveBlockSchemaMetadata(resolveType, meta);
const { vendor, app } = parsed;

return {
id: appBlockId(vendor, app),
app,
vendor,
title: metadata.title ?? appLabel(blockKey, block, meta),
description: metadata.description ?? "",
category: installedAppCategory(vendor),
logo: metadata.logo ?? metadata.icon,
resolveType,
blockKey,
installed: true,
};
}

/**
* Merges the deco app store, manifest schema apps, and installed decofile
* blocks — mirrors admin's Apps view data sources.
Expand All @@ -149,23 +193,18 @@ export function buildAppCatalog(
byId.set(entry.id, entry);
}

// Installed apps missing from store/schema (legacy block ids, local apps).
for (const installed of listInstalledAppEntries(decofile, meta)) {
const parsed = parseAppResolveType(installed.resolveType);
if (!parsed) continue;
const id = appBlockId(parsed.vendor, parsed.app);
if (byId.has(id)) continue;
byId.set(id, {
id,
app: parsed.app,
vendor: parsed.vendor,
title: installed.name,
description: "",
category: "Installed",
resolveType: installed.resolveType,
blockKey: installed.key,
installed: true,
});
// Installed custom/local apps and legacy block ids missing from store + manifest.
for (const [blockKey, val] of Object.entries(decofile)) {
if (blockKey.includes("/")) continue;
if (!val || typeof val !== "object" || Array.isArray(val)) continue;

const entry = catalogEntryFromInstalledBlock(
blockKey,
val as Record<string, unknown>,
meta,
);
if (!entry || byId.has(entry.id)) continue;
byId.set(entry.id, entry);
}

return [...byId.values()].sort(compareAppCatalogEntries);
Expand All @@ -180,52 +219,3 @@ function compareAppCatalogEntries(
}
return a.title.localeCompare(b.title);
}

function manifestHasApp(meta: LiveMeta, vendor: string, app: string): boolean {
const apps = meta.manifest?.blocks?.apps ?? {};
for (const alias of [
appResolveType(vendor, app),
`site/apps/${vendor}/${app}.tsx`,
`${vendor}/apps/${app}.ts`,
`${vendor}/apps/${app}.tsx`,
]) {
if (alias in apps) return true;
}
return false;
}

function listInstalledAppEntries(
decofile: Record<string, unknown>,
meta: LiveMeta,
): AppEntry[] {
const entries: AppEntry[] = [];

for (const [key, val] of Object.entries(decofile)) {
if (key.includes("/")) continue;
if (!val || typeof val !== "object" || Array.isArray(val)) continue;

const obj = val as Record<string, unknown>;
const resolveType = obj.__resolveType;
if (typeof resolveType !== "string") continue;
if (isSiteAppBlock(key, obj)) continue;

const parsed =
parseAppResolveType(resolveType) ??
(() => {
const blockIdMatch = key.match(/^([^-]+)-(.+)$/);
return blockIdMatch
? { vendor: blockIdMatch[1]!, app: blockIdMatch[2]! }
: null;
})();

if (!parsed || !manifestHasApp(meta, parsed.vendor, parsed.app)) continue;

entries.push({
key,
name: key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
resolveType,
});
}

return entries;
}
Loading
Loading