Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
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
97 changes: 96 additions & 1 deletion extensions/packageManager/src/components/FeedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
IconButton,
ActionButton,
TextField,
Toggle,
Dropdown,
} from 'office-ui-fabric-react';
import { useState, useEffect, Fragment } from 'react';
import { useApplicationApi, useTelemetryClient, TelemetryClient } from '@bfc/extension-client';
Expand Down Expand Up @@ -42,6 +44,18 @@ export interface WorkingModalProps {
closeDialog: any;
onUpdateFeed: any;
}

const FEED_TYPES = [
{
key: 'npm',
text: 'npm',
},
{
key: 'nuget',
text: 'NuGet',
},
];

export const FeedModal: React.FC<WorkingModalProps> = (props) => {
const [selectedItem, setSelectedItem] = useState<PackageSourceFeed | undefined>(undefined);
const [items, setItems] = useState<PackageSourceFeed[]>(props.feeds);
Expand Down Expand Up @@ -98,11 +112,33 @@ export const FeedModal: React.FC<WorkingModalProps> = (props) => {
);
},
},
{
key: 'column1',
name: 'Type',
fieldName: 'text',
minWidth: 75,
maxWidth: 75,
height: 32,
isResizable: false,
onRender: (item: PackageSourceFeed) => {
if (!selectedItem || item.key !== selectedItem.key || !editRow) return <DisplayField text={item.type} />;
return (
<Dropdown
disabled={!selectedItem || selectedItem.readonly}
options={FEED_TYPES}
selectedKey={selectedItem?.type}
onChange={(event, item) => {
updateSelected('type')(event, item.key);
}}
/>
);
},
},
{
key: 'column2',
name: 'URL',
fieldName: 'url',
minWidth: 300,
minWidth: 200,
isResizable: false,
height: 32,
onRender: (item: PackageSourceFeed) => {
Expand All @@ -120,6 +156,46 @@ export const FeedModal: React.FC<WorkingModalProps> = (props) => {
},
{
key: 'column3',
name: 'Filter',
fieldName: 'defaultQuery.query',
minWidth: 200,
isResizable: false,
height: 32,
onRender: (item: PackageSourceFeed) => {
if (!selectedItem || item.key !== selectedItem.key || !editRow)
return <DisplayField text={item.defaultQuery?.query} />;
return (
<TextField
disabled={!selectedItem || selectedItem.readonly}
placeholder={formatMessage('Default Query')}
styles={{ field: { fontSize: 12 } }}
value={selectedItem ? selectedItem.defaultQuery?.query : ''}
onChange={updateSelectedDefaultQuery('query')}
/>
);
},
},
{
key: 'column4',
name: 'Prerelease',
fieldName: 'defaultQuery.prerelease',
minWidth: 40,
maxWidth: 40,
isResizable: false,
height: 32,
onRender: (item: PackageSourceFeed) => {
return (
<Toggle
ariaLabel={formatMessage('Include prerelease versions')}
checked={item ? item.defaultQuery?.prerelease : false}
disabled={!selectedItem || selectedItem.readonly}
onChange={updateSelectedDefaultQuery('prerelease')}
/>
);
},
},
{
key: 'column5',
minWidth: 40,
maxWidth: 40,
isResizable: false,
Expand Down Expand Up @@ -150,6 +226,20 @@ export const FeedModal: React.FC<WorkingModalProps> = (props) => {
};
};

const updateSelectedDefaultQuery = (field: string) => {
return (evt, val) => {
const newSelection = {
...selectedItem,
defaultQuery: {
...selectedItem.defaultQuery,
[field]: val,
},
};
setSelectedItem(newSelection);
setItems(items.map((i) => (i.key === newSelection.key ? newSelection : i)));
};
};

const savePendingEdits = () => {
props.onUpdateFeed(items);
};
Expand All @@ -159,6 +249,11 @@ export const FeedModal: React.FC<WorkingModalProps> = (props) => {
key: uuid(),
text: '',
url: '',
defaultQuery: {
semVerLevel: '2.0.0',
prerelease: false,
query: '',
},
} as PackageSourceFeed;

const newItems = items.concat([newItem]);
Expand Down
5 changes: 2 additions & 3 deletions extensions/packageManager/src/node/feeds/feedInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ export interface IPackageSource {
key: string;
text: string;
url: string;
searchUrl?: string;
readonly?: boolean;
defaultQuery?: IPackageQuery;
type?: PackageSourceType.NuGet | PackageSourceType.NPM;
defaultQuery: IPackageQuery;
type: PackageSourceType.NuGet | PackageSourceType.NPM;
}

/**
Expand Down
32 changes: 31 additions & 1 deletion extensions/packageManager/src/node/feeds/npm/npmFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export class NpmFeed implements IFeed {
throw new Error('Package source or data should be provided');
}

const httpResponse = await axios.get(this.packageSource.url);
const npmGetSearchUrl = this.buildNPMGetSearchUrl(this.packageSource.url, query);
const httpResponse = await axios.get(npmGetSearchUrl);
const feed = httpResponse?.data;

if (!feed) {
Expand Down Expand Up @@ -72,4 +73,33 @@ export class NpmFeed implements IFeed {
};
});
}

/**
* Build npm search url based on the NuGet search spec.
* Spec: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#get-v1search
* @param baseUrl The NPM search service url.
* @param query The desired query parameters. Note that if a package source provides a query, that query will be prioritized.
* @todo Eventually que parameter query should augment the package query to support paging and further filtering. So the effective query will be a combination of the
* package query plus the specified query parameters.
*/
private buildNPMGetSearchUrl(baseUrl: string, query?: IPackageQuery): string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice that now we have this one hooked up too!

let url = `${baseUrl}?`;

if (query?.query) {
url = `${url}&text=${query.query}`;
if (!query?.prerelease) {
url = `${url}+not:unstable`;
}
}

if (query?.take) {
url = `${url}&size=${query.take}`;
}

if (query?.skip) {
url = `${url}&from=${query.skip}`;
}

return url;
}
}
34 changes: 22 additions & 12 deletions extensions/packageManager/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import formatMessage from 'format-message';
import { IExtensionRegistration } from '@botframework-composer/types';
import { SchemaMerger } from '@microsoft/bf-dialog/lib/library/schemaMerger';

import { IFeed, IPackageDefinition, IPackageQuery, IPackageSource, PackageSourceType } from './feeds/feedInterfaces';
import { IFeed, IPackageDefinition, IPackageSource, PackageSourceType } from './feeds/feedInterfaces';
import { FeedFactory } from './feeds/feedFactory';

const API_ROOT = '/api';
Expand Down Expand Up @@ -132,16 +132,24 @@ export default async (composer: IExtensionRegistration): Promise<void> => {
{
key: 'npm',
text: formatMessage('npm'),
url: `https://registry.npmjs.org/-/v1/search?text=keywords:${botComponentTag}+scope:microsoft&size=100&from=0`,
searchUrl: `https://registry.npmjs.org/-/v1/search?text={{keyword}}+keywords:${botComponentTag}&size=100&from=0`,
url: `https://registry.npmjs.org/-/v1/search`,
readonly: true,
defaultQuery: {
prerelease: true,
query: `keywords:${botComponentTag}+scope:microsoft`,
},
type: PackageSourceType.NPM,
},
{
key: 'npm-community',
text: formatMessage('JS community packages'),
url: `https://registry.npmjs.org/-/v1/search?text=keywords:${botComponentTag}&size=100&from=0`,
searchUrl: `https://registry.npmjs.org/-/v1/search?text={{keyword}}+keywords:${botComponentTag}&size=100&from=0`,
url: `https://registry.npmjs.org/-/v1/search`,
readonly: true,
defaultQuery: {
prerelease: true,
query: `keywords:${botComponentTag}`,
},
type: PackageSourceType.NPM,
},
];

Expand Down Expand Up @@ -209,15 +217,17 @@ export default async (composer: IExtensionRegistration): Promise<void> => {
if (packageSource) {
try {
const feed: IFeed = await new FeedFactory(composer).build(packageSource);
const packageQuery: IPackageQuery = {
prerelease: true,
semVerLevel: '2.0.0',
query: 'tags:msbot-component',
};

composer.log('GETTING FEED', packageSource, packageSource.defaultQuery ?? packageQuery);
// append user specified query to defaultQuery
if (req.query.term) {
packageSource.defaultQuery.query = `${packageSource.defaultQuery.query}+${encodeURIComponent(
req.query.term
)}`;
}

composer.log('GETTING FEED', packageSource, packageSource.defaultQuery);

const packages = await feed.getPackages(packageSource.defaultQuery ?? packageQuery);
const packages = await feed.getPackages(packageSource.defaultQuery);

if (Array.isArray(packages)) {
combined.push(...packages);
Expand Down
13 changes: 8 additions & 5 deletions extensions/packageManager/src/pages/Library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ export interface PackageSourceFeed extends IDropdownOption {
name: string;
key: string;
url: string;
searchUrl?: string;
type: string;
defaultQuery?: {
prerelease: boolean;
semVerLevel: string;
query: string;
};
readonly?: boolean;
}

Expand Down Expand Up @@ -444,10 +449,8 @@ const Library: React.FC = () => {
telemetryClient.track('PackageSearch', { term: searchTerm });

const response = await getSearchResults();
// if we are searching, but there is not a searchUrl, apply a local filter
if (!feeds.find((f) => f.key === feed)?.searchUrl) {
response.data.available = response.data.available.filter(applySearchTerm);
}
// if we are searching, apply a local filter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this comment still apply?

response.data.available = response.data.available.filter(applySearchTerm);
updateAvailableLibraries(response.data.available);
setRecentlyUsed(response.data.recentlyUsed);
} else {
Expand Down