Skip to content
Open
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
1 change: 1 addition & 0 deletions pxtlib/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1839,6 +1839,7 @@ namespace ts.pxtc.service {
imageUrl?: string;
type?: ExtensionType;
learnMoreUrl?: string;
installed?: boolean;

pkgConfig?: pxt.PackageConfig; // Added if the type is Bundled
repo?: pxt.github.GitRepo; //Added if the type is Github VVN TODO ADD THIS
Expand Down
18 changes: 14 additions & 4 deletions react-common/components/extensions/ExtensionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ export interface ExtensionCardProps<U> {
imageUrl?: string;
learnMoreUrl?: string;
label?: string;
labelClass?: string;
onClick?: (value: U) => void;
extension?: U;
loading?: boolean;
installed?: boolean;
showDisclaimer?: boolean
}

Expand All @@ -23,9 +25,11 @@ export const ExtensionCard = <U,>(props: ExtensionCardProps<U>) => {
imageUrl,
learnMoreUrl,
label,
labelClass,
onClick,
extension,
loading,
installed,
showDisclaimer
} = props;

Expand All @@ -34,26 +38,32 @@ export const ExtensionCard = <U,>(props: ExtensionCardProps<U>) => {
}

const id = pxt.Util.guidGen();
const cardLabel = installed ? lf("Installed") : label;
const cardLabelClass = installed ? classList("installed", labelClass) : labelClass;
const descriptionId = id + "-description";
const statusId = cardLabel ? id + "-status" : undefined;

return <>
<Card
className={classList("common-extension-card", loading && "loading")}
className={classList("common-extension-card", loading && "loading", installed && "installed")}
onClick={onCardClick}
ariaLabelledBy={id + "-title"}
ariaDescribedBy={id + "-description"}
ariaDescribedBy={classList(descriptionId, statusId)}
tabIndex={onClick && 0}
label={label}>
label={cardLabel}
labelClass={cardLabelClass}>
<div className="common-extension-card-contents">
{!loading && <>
{imageUrl && <LazyImage src={imageUrl} alt={title} />}
<div className="common-extension-card-title" id={id + "-title"} title={title}>
{title}
</div>
<div className="common-extension-card-description">
<div id={id + "-description"}>
<div id={descriptionId}>
{description}
</div>
</div>
{cardLabel && <div id={statusId} className="sr-only">{cardLabel}</div>}
{(showDisclaimer || learnMoreUrl) &&
<div className="common-extension-card-extra-content">
{showDisclaimer && lf("User-provided extension, not endorsed by Microsoft.")}
Expand Down
20 changes: 20 additions & 0 deletions react-common/styles/extensions/ExtensionCard.less
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
border-bottom-right-radius: 0.5rem;
}

.common-card-label.installed {
color: var(--pxt-colors-green-foreground);
background-color: var(--pxt-colors-green-background);
border-color: var(--pxt-colors-green-hover);
}

a.link-button {
float: right;
position: relative;
Expand Down Expand Up @@ -94,6 +100,14 @@
}
}

.common-extension-card.installed {
overflow: visible;

.common-card-body {
overflow: hidden;
}
}

/****************************************************
* High Contrast *
****************************************************/
Expand All @@ -102,5 +116,11 @@
.common-extension-card {
border-color: @highContrastTextColor;
background-color: @highContrastBackgroundColor;

.common-card-label.installed {
color: @highContrastBackgroundColor;
background-color: @highContrastTextColor;
border-color: @highContrastTextColor;
}
}
}
111 changes: 95 additions & 16 deletions webapp/src/extensionsBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Modal } from "../../react-common/components/controls/Modal";
import { classList } from "../../react-common/components/util";
import { TabList, TabListProps } from "../../react-common/components/controls/TabList";

type ExtensionMeta = pxtc.service.ExtensionMeta;
type ExtensionMeta = pxtc.service.ExtensionMeta & { installed?: boolean };
const ExtensionType = pxtc.service.ExtensionType;
type EmptyCard = { name: string, loading?: boolean }
const emptyCard: EmptyCard = { name: "", loading: true }
Expand Down Expand Up @@ -58,6 +58,76 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => {
}
}, [searchFor])

function currentProjectDependencies(): pxt.Map<string> {
return pkg.mainPkg?.config?.dependencies || {};
}

function dependencyVersions(): string[] {
const dependencies = currentProjectDependencies();
return Object.keys(dependencies).map(dep => dependencies[dep]);
}

function isDependencyInstalled(name?: string): boolean {
return !!name && !!currentProjectDependencies()[name];
}

function normalizedPublishedScriptId(version: string): string | undefined {
if (!version) return undefined;
if (version.slice(0, 4) === "pub:") return version.slice(4);
return pxt.Cloud.parseScriptId(version);
}

function githubReposMatch(a?: pxt.github.ParsedRepo, b?: pxt.github.ParsedRepo): boolean {
if (!a || !b) return false;
if (a.fileName || b.fileName)
return a.fullName.toLowerCase() === b.fullName.toLowerCase();
return a.slug.toLowerCase() === b.slug.toLowerCase();
}

function isGithubExtensionInstalled(extensionInfo: ExtensionMeta): boolean {
const extensionRepos: pxt.github.ParsedRepo[] = [];
const repoIds = [extensionInfo.repo?.fullName, extensionInfo.fullRepo];
repoIds.forEach(repoId => {
const parsed = repoId && pxt.github.parseRepoId(repoId);
if (parsed) extensionRepos.push(parsed);
});
if (!extensionRepos.length) return false;

return dependencyVersions().some(version => {
const dependencyRepo = pxt.github.parseRepoId(version);
return extensionRepos.some(repo => githubReposMatch(repo, dependencyRepo));
});
}

function isShareScriptInstalled(scriptInfo?: pxt.Cloud.JsonScript): boolean {
if (!scriptInfo?.id) return false;
return dependencyVersions().some(version => normalizedPublishedScriptId(version) === scriptInfo.id);
}

function isLocalExtensionInstalled(header: pxt.workspace.Header): boolean {
return dependencyVersions().some(version => version === `workspace:${header.id}`);
}

function isExtensionInstalled(extensionInfo: ExtensionMeta): boolean {
switch (extensionInfo.type) {
case ExtensionType.Bundled:
return isDependencyInstalled(extensionInfo.pkgConfig?.name || extensionInfo.name);
case ExtensionType.Github:
return isGithubExtensionInstalled(extensionInfo) || isDependencyInstalled(extensionInfo.name);
case ExtensionType.ShareScript:
return isShareScriptInstalled(extensionInfo.scriptInfo) || isDependencyInstalled(extensionInfo.name);
default:
return isDependencyInstalled(extensionInfo.name);
}
}

function withInstalledFlag<T extends ExtensionMeta>(extensionInfo: T): T {
return {
...extensionInfo,
installed: isExtensionInstalled(extensionInfo)
};
}

/**
* Github search
*/
Expand Down Expand Up @@ -106,10 +176,17 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => {
if (!newExtension) {
return;
}
const addedExtensions = allExtensions;
const addedExtensions = new Map(allExtensions);
newExtension.forEach(e => {
if (!addedExtensions.has(e.name.toLowerCase())) {
addedExtensions.set(e.name.toLowerCase(), e)
const extensionWithStatus = withInstalledFlag(e);
if (!addedExtensions.has(extensionWithStatus.name.toLowerCase())) {
addedExtensions.set(extensionWithStatus.name.toLowerCase(), extensionWithStatus)
}
if (extensionWithStatus.fullRepo && !addedExtensions.has(extensionWithStatus.fullRepo.toLowerCase())) {
addedExtensions.set(extensionWithStatus.fullRepo.toLowerCase(), extensionWithStatus)
}
if (extensionWithStatus.repo?.fullName && !addedExtensions.has(extensionWithStatus.repo.fullName.toLowerCase())) {
addedExtensions.set(extensionWithStatus.repo.fullName.toLowerCase(), extensionWithStatus)
}
})
setAllExtensions(addedExtensions);
Expand Down Expand Up @@ -308,25 +385,25 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => {
}

function parseGithubRepo(r: pxt.github.GitRepo): ExtensionMeta {
return {
return withInstalledFlag({
name: ghName(r),
displayName: r.displayName,
type: ExtensionType.Github,
imageUrl: pxt.github.repoIconUrl(r),
repo: r,
description: r.description,
fullRepo: r.fullName
}
})
}

function parseShareScript(s: pxt.Cloud.JsonScript): ExtensionMeta {
return {
return withInstalledFlag({
name: s.name,
type: ExtensionType.ShareScript,
imageUrl: s.thumb ? `${pxt.Cloud.apiRoot}/${s.id}/thumb` : undefined,
description: s.description,
scriptInfo: s,
}
})
}


Expand Down Expand Up @@ -364,15 +441,15 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => {
}

function packageConfigToExtensionMeta(p: pxt.PackageConfig): ExtensionMeta {
return {
return withInstalledFlag({
name: p.name,
displayName: p.displayName,
imageUrl: p.icon,
type: ExtensionType.Bundled,
learnMoreUrl: `/reference/${p.name}`,
pkgConfig: p,
description: p.description
}
})
}

function fetchBundled(): Map<string, ExtensionMeta> {
Expand All @@ -385,7 +462,6 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => {
.filter(pk => !pk.searchOnly || searchFor?.length != 0)
.filter(pk => pk.name != "core")
.filter(pk => false == !!pk.core) // show core in "boards" mode
.filter(pk => !pkg.mainPkg.deps[pk.name] || pkg.mainPkg.deps[pk.name].cppOnly) // don't show package already referenced in extensions
.sort((a, b) => {
// core first
if (a.core != b.core)
Expand Down Expand Up @@ -457,7 +533,7 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => {
function ExtensionMetaCard(props: {
extensionInfo: ExtensionMeta & EmptyCard,
}) {
const { extensionInfo } = props;
const extensionInfo = withInstalledFlag(props.extensionInfo);
const {
description,
fullRepo,
Expand All @@ -478,6 +554,7 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => {
onClick={installExtension}
learnMoreUrl={learnMoreUrl || (fullRepo ? `/pkg/${fullRepo}` : undefined)}
loading={loading}
installed={extensionInfo.installed}
label={pxt.isPkgBeta(extensionInfo) ? lf("Beta") : undefined}
showDisclaimer={type != ExtensionType.Bundled && repo?.status != pxt.github.GitRepoStatus.Approved}
/>;
Expand Down Expand Up @@ -578,16 +655,18 @@ export const ExtensionsBrowser = (props: ExtensionsProps) => {
)
}
{currentTab === LOCAL_TAG_ID &&
extensionsInDevelopment.map((p, index) =>
<ExtensionCard
extensionsInDevelopment.map((p, index) => {
const installed = isLocalExtensionInstalled(p);
return <ExtensionCard
key={`local:${index}`}
title={p.name}
description={lf("Local copy of {0} hosted on github.com", p.githubId)}
imageUrl={p.icon}
extension={p}
onClick={addLocal}
/>
)
installed={installed}
/>;
})
}
{currentTab !== RECOMMENDED_TAG_ID && currentTab !== LOCAL_TAG_ID &&
extensionsToShow?.map(
Expand Down
Loading