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 localtypings/pxteditor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@ declare namespace pxt.editor {
createProjectAsync(options: pxt.editor.ProjectCreationOptions): Promise<void>;
importExampleAsync(options: ExampleImportOptions): Promise<void>;
showScriptManager(): void;
showGalleryViewer(galleryPath: string, galleryName: string): void;
importProjectDialog(): void;
removeProject(): void;
editText(): void;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pxt-core",
"version": "11.4.7",
"version": "11.4.8",
"description": "Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors",
"keywords": [
"TypeScript",
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/galleryviewer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { expect } = require('chai');
const sinon = require('sinon');

describe('View All logic', () => {
it('calls showGalleryViewer when gallery View All is clicked', () => {
// Mock gallery structure
const galleryName = "Tutorials";
const galleryUrl = "tutorials";

// Simulate a simplified Projects component with props
const parentMock = {
showGalleryViewer: sinon.spy()
};

// Simulate what the click handler does
function onClickViewAll(url, name) {
if (url && parentMock.showGalleryViewer) {
parentMock.showGalleryViewer(url, name);
}
}

// Act: pretend user clicked
onClickViewAll(galleryUrl, galleryName);

// Assert
expect(parentMock.showGalleryViewer.calledOnce).to.be.true;
expect(parentMock.showGalleryViewer.firstCall.args).to.deep.equal(["tutorials", "Tutorials"]);
});
});
11 changes: 11 additions & 0 deletions webapp/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import * as scriptsearch from "./scriptsearch";
import * as extensionsBrowser from "./extensionsBrowser";
import * as projects from "./projects";
import * as scriptmanager from "./scriptmanager";
import * as galleryviewer from "./galleryviewer";
import * as extensions from "./extensions";
import * as sounds from "./sounds";
import * as make from "./make";
Expand Down Expand Up @@ -144,6 +145,7 @@ export class ProjectView
shareEditor: share.ShareEditor;
languagePicker: lang.LanguagePicker;
scriptManagerDialog: scriptmanager.ScriptManagerDialog;
galleryViewerDialog: galleryviewer.GalleryViewerDialog;
importDialog: projects.ImportDialog;
loginDialog: identity.LoginDialog;
profileDialog: user.ProfileDialog;
Expand Down Expand Up @@ -4126,6 +4128,10 @@ export class ProjectView
this.scriptManagerDialog.show();
}

showGalleryViewer(galleryPath: string, galleryName: string) {
this.galleryViewerDialog.show(galleryPath, galleryName);
}

importProjectDialog() {
this.importDialog.show();
}
Expand Down Expand Up @@ -5297,6 +5303,10 @@ export class ProjectView
this.scriptManagerDialog = c;
}

private handleGalleryViewerDialogRef = (c: galleryviewer.GalleryViewerDialog) => {
this.galleryViewerDialog = c;
}

private handleImportDialogRef = (c: projects.ImportDialog) => {
this.importDialog = c;
}
Expand Down Expand Up @@ -5499,6 +5509,7 @@ export class ProjectView
{hasIdentity ? <identity.LoginDialog parent={this} ref={this.handleLoginDialogRef} /> : undefined}
{hasIdentity ? <user.ProfileDialog parent={this} ref={this.handleProfileDialogRef} /> : undefined}
{inHome && targetTheme.scriptManager ? <scriptmanager.ScriptManagerDialog parent={this} ref={this.handleScriptManagerDialogRef} onClose={this.handleScriptManagerDialogClose} /> : undefined}
<galleryviewer.GalleryViewerDialog parent={this} ref={this.handleGalleryViewerDialogRef} />
{sandbox ? undefined : <projects.ExitAndSaveDialog parent={this} ref={this.handleExitAndSaveDialogRef} />}
{sandbox ? undefined : <projects.NewProjectDialog parent={this} ref={this.handleNewProjectDialogRef} />}
{hwDialog ? <projects.ChooseHwDialog parent={this} ref={this.handleChooseHwDialogRef} /> : undefined}
Expand Down
139 changes: 139 additions & 0 deletions webapp/src/galleryviewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import * as React from "react";
import * as data from "./data";
import * as sui from "./sui";
import * as core from "./core";
import * as codecard from "./codecard";

import { SearchInput } from "./components/searchInput";
import { fireClickOnEnter } from "./util";

import ISettingsProps = pxt.editor.ISettingsProps;
import { applyCodeCardAction } from "./projects";

export interface GalleryViewerDialogProps extends ISettingsProps {
onClose?: () => void;
}

export interface GalleryViewerDialogState {
visible?: boolean;
galleryCards?: pxt.CodeCard[];
searchFor?: string;
galleryName?: string;
galleryPath?: string;
}

export class GalleryViewerDialog extends data.Component<GalleryViewerDialogProps, GalleryViewerDialogState> {
constructor(props: GalleryViewerDialogProps) {
super(props);
this.state = {
visible: false,
galleryCards: [],
galleryName: "",
galleryPath: ""
};

this.close = this.close.bind(this);
this.handleCardClick = this.handleCardClick.bind(this);
this.handleSearch = this.handleSearch.bind(this);
}

hide() {
this.setState({ visible: false, searchFor: undefined });
}

close() {
this.setState({ visible: false, searchFor: undefined });
if (this.props.onClose) this.props.onClose();
}

show(galleryPath: string, galleryName: string) {
this.setState({
visible: true,
galleryPath: galleryPath,
galleryName: galleryName,
galleryCards: [] // Clear previous gallery cards
}, () => {
this.fetchGalleryData();
});
}

fetchGalleryData() {
const { galleryPath } = this.state;
if (!galleryPath) return;

// Fetch gallery cards
let res = this.getData(`gallery:${encodeURIComponent(galleryPath)}`) as pxt.gallery.Gallery[];
if (res && !(res instanceof Error)) {
this.setState({
galleryCards: pxt.Util.concat(res.map(g => g.cards))
});
} else {
// Handle error
this.setState({
galleryCards: []
});
}
}

handleCardClick(e: any, scr: pxt.CodeCard) {
pxt.tickEvent("galleryviewer.card", { name: scr.name, cardType: scr.cardType });

// Use the same application method as the regular gallery
applyCodeCardAction(this.props.parent, "projects", scr);

// Close the dialog after selection
this.close();
}

handleSearch(inputValue: string) {
const searchFor = (inputValue || '').trim().toLowerCase();
this.setState({ searchFor });
}

renderCore() {
const { visible, galleryCards, searchFor, galleryName } = this.state;
if (!visible) return <div />;

// Filter gallery cards based on search term if provided
const filteredCards = searchFor && galleryCards ?
galleryCards.filter(card =>
(card.name || "").toLowerCase().indexOf(searchFor) !== -1 ||
(card.description || "").toLowerCase().indexOf(searchFor) !== -1
) :
galleryCards || [];

return (
<sui.Modal isOpen={visible} className="galleryviewer" size="large"
onClose={this.close} dimmer={true}
closeIcon={true} header={lf("All {0}", galleryName)}
closeOnDimmerClick closeOnDocumentClick closeOnEscape>
<div className="ui">
<div className="ui small fluid icon input searchdialog">
<SearchInput defaultValue={searchFor} placeholder={lf("Search...")} searchHandler={this.handleSearch} autoFocus={true} />
</div>
{filteredCards.length === 0 ?
<div className="ui segment">
{searchFor ? lf("No matching tutorials found.") : lf("No tutorials found.")}
</div> :
<div className="ui cards centered gallery-cards">
{filteredCards.map((card, index) =>
<codecard.CodeCardView
key={card.name || card.url || index}
name={card.name}
ariaLabel={card.name}
description={card.description}
imageUrl={card.imageUrl}
youTubeId={card.youTubeId}
label={card.label}
labelClass={card.labelClass}
tags={card.tags}
onClick={(e) => this.handleCardClick(e, card)}
/>
)}
</div>
}
</div>
</sui.Modal>
);
}
}
58 changes: 55 additions & 3 deletions webapp/src/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,32 @@ export class Projects extends auth.Component<ISettingsProps, ProjectsState> {
const url = typeof galProps === "string" ? galProps : galProps.url
const shuffle: pxt.GalleryShuffle = typeof galProps === "string" ? undefined : galProps.shuffle;
return <div key={`${galleryName}_gallerysegment`} className="ui segment gallerysegment" role="region" aria-label={pxt.Util.rlf(galleryName)}>
<h2 className="ui header heading">{pxt.Util.rlf(galleryName)} </h2>
<div className="ui heading">
<div className="column" style={{ zIndex: 1 }}
onClick={() => url && this.props.parent.showGalleryViewer(url, galleryName)}
onKeyDown={fireClickOnEnter}
>
<h2 className="ui header myproject-header">
{pxt.Util.rlf(galleryName)}
<span
className="view-all-button"
tabIndex={0}
title={lf("View all {0}", galleryName)}
role="button"
>
{lf("View All")}
</span>
</h2>
</div>
</div>
<div className="content">
<ProjectsCarousel ref={`${selectedCategory == galleryName ? 'activeCarousel' : ''}`}
key={`${galleryName}_carousel`} parent={this.props.parent}
name={galleryName}
path={url}
onClick={this.chgGallery} setSelected={this.setSelected}
shuffle={shuffle}
showViewAll={false}
selectedIndex={selectedCategory == galleryName ? selectedIndex : undefined} />
</div>
</div>
Expand Down Expand Up @@ -570,6 +588,7 @@ interface ProjectsCarouselProps extends ISettingsProps {
selectedIndex?: number;
setSelected?: (name: string, index: number) => void;
shuffle?: pxt.GalleryShuffle;
showViewAll?: boolean;
}

interface ProjectsCarouselState {
Expand All @@ -591,6 +610,7 @@ export class ProjectsCarousel extends data.Component<ProjectsCarouselProps, Proj
this.closeDetailOnEscape = this.closeDetailOnEscape.bind(this);
this.reload = this.reload.bind(this);
this.showScriptManager = this.showScriptManager.bind(this);
this.handleViewAllClick = this.handleViewAllClick.bind(this);
this.handleCardClick = this.handleCardClick.bind(this);
}

Expand Down Expand Up @@ -639,6 +659,17 @@ export class ProjectsCarousel extends data.Component<ProjectsCarouselProps, Proj
this.props.parent.showScriptManager();
}

handleViewAllClick(e: React.MouseEvent<HTMLSpanElement>) {
e.stopPropagation();
e.preventDefault();

const { name, path } = this.props;
if (path) {
pxt.tickEvent("gallery.viewall", { gallery: name });
this.props.parent.showGalleryViewer(path, name);
}
}

closeDetail() {
const { name } = this.props;
pxt.tickEvent("projects.detail.close");
Expand Down Expand Up @@ -705,7 +736,28 @@ export class ProjectsCarousel extends data.Component<ProjectsCarouselProps, Proj
} else {
const selectedElement = cards[selectedIndex];
const hasTags = cards.some(c => c.tags && c.tags.length != 0)
return <div>
return <div className="ui segment gallery-segment">
{this.props.showViewAll && (
<div className="ui grid gallery-grid">
<div className="ui row gallery-header">
<div className="column">
<h2 className="ui header gallery-title">
{lf(name)}
<span
className="view-all-button"
tabIndex={0}
title={lf("View all {0}", name)}
role="button"
onClick={this.handleViewAllClick}
onKeyDown={fireClickOnEnter}
>
{lf("View All")}
</span>
</h2>
</div>
</div>
</div>
)}
<carousel.Carousel ref="carousel" tickId={path} bleedPercent={20} selectedIndex={selectedIndex}>
{cards.map((scr, index) =>
<ProjectsCodeCard
Expand Down Expand Up @@ -1195,7 +1247,7 @@ function cardActionButton(props: Partial<ProjectsDetailProps>, className: string
/>
}

function applyCodeCardAction(projectView: IProjectView, ticSrc: "projects" | "herobanner", scr: pxt.CodeCard, action?: pxt.CodeCardAction) {
export function applyCodeCardAction(projectView: IProjectView, ticSrc: "projects" | "herobanner", scr: pxt.CodeCard, action?: pxt.CodeCardAction) {
let editor: string = (action && action.editor) || "blocks";
if (editor == "js") editor = "ts";
const url = action ? action.url : scr.url;
Expand Down
Loading