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
28 changes: 27 additions & 1 deletion webapp/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4829,13 +4829,39 @@ export class ProjectView
async hasBlocksFromExtensionAsync(dependencyName: string): Promise<boolean> {
const blocksInfo = await compiler.getBlocksAsync();
const workspaceBlocks = this.blocksEditor?.editor?.getAllBlocks(false) || [];
const removedPackageIds = this.getPackageIdsRemovedWithDependency(dependencyName);

return workspaceBlocks.some(block => {
const symbol = blocksInfo.blocksById[block.type] || pxtblockly.blockSymbol(block.type);
return symbol?.pkg === dependencyName;
return removedPackageIds.has(symbol?.pkg);
});
}

private getPackageIdsRemovedWithDependency(dependencyName: string): Set<string> {
const removedPackageIds = new Set<string>([dependencyName]);
const dependency = pkg.mainPkg?.resolveDep(dependencyName);
if (!dependency) return removedPackageIds;

const dependencyPackages = new Set<pxt.Package>();
const retainedPackages = new Set<pxt.Package>();
const visit = (pack: pxt.Package, visited: Set<pxt.Package>) => {
if (!pack || visited.has(pack)) return;
visited.add(pack);
pack.resolvedDependencies().forEach(child => visit(child, visited));
};

visit(dependency, dependencyPackages);
Object.keys(pkg.mainPkg.dependencies())
.filter(name => name !== dependencyName)
.map(name => pkg.mainPkg.resolveDep(name))
.forEach(pack => visit(pack, retainedPackages));

dependencyPackages.forEach(pack => {
if (!retainedPackages.has(pack)) removedPackageIds.add(pack.id);
});
return removedPackageIds;
}

showBoardDialogAsync(features?: string[], closeIcon?: boolean): Promise<void> {
return this.scriptSearch.showBoardsAsync(features, closeIcon);
}
Expand Down
14 changes: 7 additions & 7 deletions webapp/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ interface BundledPackage {
}

interface UsedPackageInfo {
dirname: string,
packageId: string,
info: pxt.PackageApiInfo
}

Expand Down Expand Up @@ -678,7 +678,7 @@ async function getCachedApiInfoAsync(project: pkg.EditorPackage, bundled: pxt.Ma
const usedPackages = project.pkgAndDeps();
const externalPackages: pkg.EditorPackage[] = [];
const usedPackageInfo: UsedPackageInfo[] = [{
dirname: corePkgName,
packageId: pxt.appTarget.corepkg,
info: corePkg
}];

Expand All @@ -689,7 +689,7 @@ async function getCachedApiInfoAsync(project: pkg.EditorPackage, bundled: pxt.Ma
for (const bundle of bundledPackages) {
if (bundle.config.name === getPackageKey(dep)) {
usedPackageInfo.push({
dirname: bundle.dirname,
packageId: dep.getPkgId(),
info: bundled[bundle.dirname]
});
foundIt = true;
Expand Down Expand Up @@ -721,7 +721,7 @@ async function getCachedApiInfoAsync(project: pkg.EditorPackage, bundled: pxt.Ma
else {
pxt.debug(`Fetched cached API info for ${getPackageKey(dep)}`);
usedPackageInfo.push({
dirname: dep.getPkgId(),
packageId: dep.getPkgId(),
info: entry
});
}
Expand All @@ -734,13 +734,13 @@ async function getCachedApiInfoAsync(project: pkg.EditorPackage, bundled: pxt.Ma

for (const used of usedPackageInfo) {
if (!used) continue;
let { info, dirname } = used;
let { info, packageId } = used;

const byQName = U.cloneApis(info.apis.byQName);

// reinclude the pkg the api originates from, which is trimmed during compression
// Restore the runtime package ID, which is trimmed during compression.
for (const api of Object.keys(byQName)) {
byQName[api].pkg = dirname;
byQName[api].pkg = packageId;

// We had a bug where we were caching the translated language code and it broke translations.
// make sure we clear it on any old cached entries from before the bug was fixed
Expand Down
77 changes: 64 additions & 13 deletions webapp/src/components/tutorial/TutorialCallout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,102 @@ interface TutorialCalloutProps extends React.PropsWithChildren<{}> {
onClick?: (visible: boolean) => void;
}

interface HorizontalPosition {
left?: string;
width?: string;
}

export function TutorialCallout(props: TutorialCalloutProps) {
const { children, className, buttonIcon, buttonLabel, onClick } = props;
const [ visible, setVisible ] = React.useState(false);
const [ maxHeight, setMaxHeight ] = React.useState("unset");
const [ top, setTop ] = React.useState("unset");
const [ bottom, setBottom ] = React.useState("unset");
const [ horizontalPosition, setHorizontalPosition ] = React.useState<HorizontalPosition>({});
const popupRef = React.useRef<HTMLDivElement>(null);
const contentRef = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
React.useLayoutEffect(() => {
if (!visible) return undefined;

function checkSize() {
function updatePosition() {
const lowerBuffer = (document.getElementById("editortools")?.clientHeight ?? 0) + 30;
if (contentRef.current?.getBoundingClientRect().bottom >= window.innerHeight - lowerBuffer) {
const upperBuffer = 16;
const trigger = popupRef.current?.querySelector<HTMLElement>(".tutorial-callout-button");
const content = contentRef.current;

if (!trigger || !content) return;

const triggerBottom = trigger.getBoundingClientRect().bottom;
const contentStyle = getComputedStyle(content);
const transform = contentStyle.transform;
const verticalOffset = transform === "none" ? 0 : new DOMMatrixReadOnly(transform).m42;
const borderHeight = parseFloat(contentStyle.borderTopWidth) + parseFloat(contentStyle.borderBottomWidth);
const verticalChrome = parseFloat(contentStyle.paddingTop) + parseFloat(contentStyle.paddingBottom) + borderHeight;
const popupHeight = content.scrollHeight + borderHeight;
const availableBottom = window.innerHeight - lowerBuffer;
const availableHeight = Math.max(availableBottom - upperBuffer, 0);
const inlineLeft = content.style.left;
const inlineRight = content.style.right;
const inlineWidth = content.style.width;
content.style.left = "";
content.style.right = "";
content.style.width = "";
const contentRect = content.getBoundingClientRect();
content.style.left = inlineLeft;
content.style.right = inlineRight;
content.style.width = inlineWidth;
const clampedLeft = Math.min(
Math.max(contentRect.left, upperBuffer),
Math.max(window.innerWidth - upperBuffer - contentRect.width, upperBuffer)
);
const isHorizontallyClamped = Math.abs(clampedLeft - contentRect.left) > 0.5;
const nextHorizontalPosition = isHorizontallyClamped
? { left: `${clampedLeft}px`, width: `${contentRect.width}px` }
: {};
setHorizontalPosition(current =>
current.left === nextHorizontalPosition.left && current.width === nextHorizontalPosition.width
? current
: nextHorizontalPosition
);

if (triggerBottom + verticalOffset + popupHeight > availableBottom) {
setTop("unset");
setBottom(`${lowerBuffer}px`);
setMaxHeight("90vh");
setBottom(`${lowerBuffer + verticalOffset}px`);
setMaxHeight(popupHeight > availableHeight
? `${contentStyle.boxSizing === "border-box" ? availableHeight : Math.max(availableHeight - verticalChrome, 0)}px`
: "unset");
} else {
setBottom("unset");

// Set the top of the hint to the bottom of wrapper div (which aligns to the hint button).
const popupBottom = popupRef.current?.getBoundingClientRect().bottom;
setTop(popupBottom ? `${popupBottom}px` : "unset");
setTop(`${triggerBottom}px`);
setMaxHeight("unset");
}
}

let animationFrame: number;
const observer = new ResizeObserver(() => {
window.requestAnimationFrame(checkSize);
window.cancelAnimationFrame(animationFrame);
animationFrame = window.requestAnimationFrame(updatePosition);
});

observer.observe(document.body);
if (contentRef.current) observer.observe(contentRef.current);

checkSize();
updatePosition();

const closeOnOutsideClick = (e: PointerEvent) => {
if (!popupRef?.current?.contains(e.target as Node)) {
setVisible(false);
}
};

document.addEventListener("click", closeOnOutsideClick);
const outsideClickTimeout = window.setTimeout(() => {
document.addEventListener("click", closeOnOutsideClick);
}, 0);

return () => {
window.cancelAnimationFrame(animationFrame);
window.clearTimeout(outsideClickTimeout);
observer.disconnect();
document.removeEventListener("click", closeOnOutsideClick);
}
Expand Down Expand Up @@ -90,7 +141,7 @@ export function TutorialCallout(props: TutorialCalloutProps) {
ariaLabel={buttonTitle}
disabled={!children}
onClick={children ? handleButtonClick : undefined} />
{visible && <div ref={contentRef} className={`tutorial-callout no-select`} onClick={captureEvent} style={{top: top, bottom: bottom, maxHeight: maxHeight}}>
{visible && <div ref={contentRef} className={`tutorial-callout no-select`} onClick={captureEvent} style={{top: top, right: horizontalPosition.left === undefined ? undefined : "auto", bottom: bottom, left: horizontalPosition.left, width: horizontalPosition.width, maxHeight: maxHeight, overflowY: maxHeight === "unset" ? "visible" : "auto"}}>
<Button icon="close" className="tutorial-callout-close" onClick={closeCallout} />
{children}
</div>}
Expand Down
Loading