diff --git a/pxtlib/service.ts b/pxtlib/service.ts index 5793d1bf37d0..333d9a0fd71f 100644 --- a/pxtlib/service.ts +++ b/pxtlib/service.ts @@ -1674,8 +1674,9 @@ namespace ts.pxtc.service { export enum ExtensionType { - Bundled, - Github + Bundled = 1, + Github = 2, + ShareScript = 3, } export interface ExtensionMeta { @@ -1684,13 +1685,13 @@ namespace ts.pxtc.service { description?: string, imageUrl?: string, type?: ExtensionType + learnMoreUrl?: string; pkgConfig?: pxt.PackageConfig; // Added if the type is Bundled repo?: pxt.github.GitRepo; //Added if the type is Github VVN TODO ADD THIS - learnMoreUrl?: string; + scriptInfo?: pxt.Cloud.JsonScript } - export interface SearchInfo { id: string; name: string; diff --git a/webapp/src/extensionsBrowser.tsx b/webapp/src/extensionsBrowser.tsx index cfdadb2660b2..f32d50201c00 100644 --- a/webapp/src/extensionsBrowser.tsx +++ b/webapp/src/extensionsBrowser.tsx @@ -13,6 +13,7 @@ import { Modal } from "../../react-common/components/controls/Modal"; import { classList } from "../../react-common/components/util"; type ExtensionMeta = pxtc.service.ExtensionMeta; +const ExtensionType = pxtc.service.ExtensionType; type EmptyCard = { name: string, loading?: boolean } const emptyCard: EmptyCard = { name: "", loading: true } @@ -81,6 +82,12 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => { parsedExt.unshift(e) } }) + + const shareUrlData = await fetchShareUrlDataAsync(searchFor); + if (shareUrlData) { + parsedExt.unshift(parseShareScript(shareUrlData)); + } + addExtensionsToPool(parsedExt) setExtensionsToShow(parsedExt) setSearchComplete(true) @@ -100,23 +107,25 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => { } function getExtensionFromFetched(extensionUrl: string) { + const parsedGithubRepo = pxt.github.parseRepoId(extensionUrl); + if (parsedGithubRepo) + return allExtensions.get(parsedGithubRepo.fullName.toLowerCase()); + const fullName = allExtensions.get(extensionUrl.toLowerCase()) - if (fullName) { + if (fullName) return fullName - } - const parsedGithubRepo = pxt.github.parseRepoId(extensionUrl) - if (!parsedGithubRepo) return undefined; - return allExtensions.get(parsedGithubRepo.slug.toLowerCase()) + + return undefined; } async function addDepIfNoConflict(config: pxt.PackageConfig, version: string) { try { props.hideExtensions(); - core.showLoading("installingextension", lf("Adding extension...")) + core.showLoading("installingextension", lf("Adding extension...")); const added = await pkg.mainEditorPkg() .addDependencyAsync({ ...config, isExtension: true }, version, false) if (added) { - await pxt.Util.delay(1000) + await pxt.Util.delay(200); await props.reloadHeaderAsync(); } } @@ -152,11 +161,11 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => { let r: { version: string, config: pxt.PackageConfig }; try { core.showLoading("downloadingpackage", lf("downloading extension...")); - const pkg = getExtensionFromFetched(scr.name); + const pkg = getExtensionFromFetched(scr.repo.fullName); if (pkg) { r = await pxt.github.downloadLatestPackageAsync(pkg.repo); } else { - const res = await fetchGithubDataAsync([scr.name]); + const res = await fetchGithubDataAsync([scr.repo.fullName]); if (res && res.length > 0) { const parsed = parseGithubRepo(res[0]) addExtensionsToPool([parsed]) @@ -172,6 +181,34 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => { return await addDepIfNoConflict(r.config, r.version) } + async function fetchShareUrlDataAsync(potentialShareUrl: string): Promise { + const scriptId = pxt.Cloud.parseScriptId(potentialShareUrl); + if (!scriptId) + return undefined; + + const scriptData = await data.getAsync(`cloud-search:${scriptId}`); + + // TODO: fix typing on getAsync? it looks like it returns T or the failed network request + if ((scriptData as any).statusCode == 404) { + return undefined; + } + // unwrap array if returned as array + if (Array.isArray(scriptData)) { + return scriptData[0]; + } + + return scriptData; + } + async function addShareUrlExtension(scr: pxt.Cloud.JsonScript): Promise { + // todo: we justed used name before but that's easy to lead to conflicts? + // should this be scr.id or something as pkgid? + // todo: how to handle persistent links? right now scr.id is the current version, + // we should probably persist the s id and make it updatable with a refresh. + const shareScript = await workspace.getPublishedScriptAsync(scr.id); + const config = pxt.Util.jsonTryParse(shareScript[pxt.CONFIG_NAME]); + addDepIfNoConflict({...config, version: scr.id }, `pub:${scr.id}`); + } + async function fetchGithubDataAsync(preferredRepos: string[]): Promise { // When searching multiple repos at the same time, use 'extension-search' which caches results // for much longer than 'gh-search' @@ -208,15 +245,19 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => { function installExtension(scr: ExtensionMeta) { switch (scr.type) { - case pxtc.service.ExtensionType.Bundled: + case ExtensionType.Bundled: pxt.tickEvent("packages.bundled", { name: scr.name }); props.hideExtensions(); - addDepIfNoConflict(scr.pkgConfig, "*") + addDepIfNoConflict(scr.pkgConfig, "*"); break; - case pxtc.service.ExtensionType.Github: + case ExtensionType.Github: props.hideExtensions(); addGithubPackage(scr); break; + case ExtensionType.ShareScript: + props.hideExtensions(); + addShareUrlExtension(scr.scriptInfo); + break; } } @@ -234,7 +275,7 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => { function parseGithubRepo(r: pxt.github.GitRepo): ExtensionMeta { return { name: ghName(r), - type: pxtc.service.ExtensionType.Github, + type: ExtensionType.Github, imageUrl: pxt.github.repoIconUrl(r), repo: r, description: r.description, @@ -242,6 +283,16 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => { } } + function parseShareScript(s: pxt.Cloud.JsonScript): ExtensionMeta { + return { + name: s.name, + type: ExtensionType.ShareScript, + imageUrl: s.thumb ? `${pxt.Cloud.apiRoot}/${s.id}/thumb` : undefined, + description: s.description, + scriptInfo: s, + } + } + function getCategoryNames(): string[] { if (!extensionTags) return []; @@ -286,7 +337,7 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => { return { name: p.name, imageUrl: p.icon, - type: pxtc.service.ExtensionType.Bundled, + type: ExtensionType.Bundled, learnMoreUrl: `/reference/${p.name}`, pkgConfig: p, description: p.description @@ -377,6 +428,34 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => { } } + function ExtensionMetaCard(props: { + extensionInfo: ExtensionMeta & EmptyCard, + }) { + const { extensionInfo } = props; + const { + description, + fullName, + imageUrl, + learnMoreUrl, + loading, + name, + repo, + type, + } = extensionInfo; + + return ; + } + enum ExtensionView { Tabbed, Search, @@ -492,19 +571,9 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => { {displayMode == ExtensionView.Search && <>
- {extensionsToShow?.map((scr, index) => - )} + {extensionsToShow?.map( + (scr, index) => + )}
{searchComplete && extensionsToShow.length == 0 &&
@@ -514,33 +583,14 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => { } {displayMode == ExtensionView.Tags &&
- {extensionsToShow?.map((scr, index) => - )} + {extensionsToShow?.map( + (scr, index) => + )}
} {displayMode == ExtensionView.Tabbed &&
- {currentTab == TabState.Recommended && preferredExts.map((scr, index) => - + {currentTab == TabState.Recommended && preferredExts.map( + (scr, index) => )} {currentTab == TabState.InDevelopment && extensionsInDevelopment.map((p, index) =>