Skip to content

Commit

Permalink
basic UI
Browse files Browse the repository at this point in the history
  • Loading branch information
AJIXuMuK committed Apr 5, 2021
1 parent e59fb52 commit febe234
Show file tree
Hide file tree
Showing 4 changed files with 319 additions and 0 deletions.
78 changes: 78 additions & 0 deletions src/controls/sitePicker/ISitePicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { BaseComponentContext } from '@microsoft/sp-component-base';

export interface ISite {
/**
* ID of the site
*/
id?: string;
/**
* Title
*/
title?: string;
/**
* Base URL
*/
url?: string;

/**
* ID of the web
*/
webId?: string;

/**
* ID of the hub site
*/
hubSiteId?: string;
}

export interface ISitePickerProps {
/**
* Site picker label
*/
label?: string;
/**
* Specify if the control needs to be disabled
*/
disabled?: boolean;
/**
* Web Part context
*/
context: BaseComponentContext;
/**
* Intial data to load in the 'Selected sites' area (optional)
*/
initialSites?: ISite[];
/**
* Define if you want to allow multi site selection. True by default.
*/
multiSelect?: boolean;
/**
* Defines what entities are available for selection: site collections, sites, hub sites.
*/
mode?: 'site' | 'web' | 'hub';

/**
* Specifies if the options should be limited by the current site collections. Taken into consideration if selectionMode is set to 'web'
*/
limitToCurrentSiteCollection?: boolean;

/**
* Specifies if search box is displayed for the component. Default: true
*/
allowSearch?: boolean;

/**
* Specifices if the list is sorted by title or url. Default: title
*/
orderBy?: 'title' | 'url';

/**
* Specifies if the list is sorted in descending order. Default: false
*/
isDesc?: boolean;

/**
* selection change handler
*/
onChange: (selectedSites: ISite[]) => void;
}
138 changes: 138 additions & 0 deletions src/controls/sitePicker/SitePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import * as React from 'react';
import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import { ISite, ISitePickerProps } from './ISitePicker';
import { getAllSites, getHubSites } from '../../services/SPSitesService';
import { IDropdownOption, Dropdown } from 'office-ui-fabric-react/lib/Dropdown';
import { SelectableOptionMenuItemType } from 'office-ui-fabric-react/lib/utilities/selectableOption/SelectableOption.types';
import orderBy from 'lodash/orderBy';

const styles = mergeStyleSets({
loadingSpinnerContainer: {
width: '100%',
textAlign: 'center'
}
});

export const SitePicker: React.FunctionComponent<ISitePickerProps> = (props: React.PropsWithChildren<ISitePickerProps>) => {

const {
label,
disabled,
context,
initialSites,
multiSelect,
mode,
limitToCurrentSiteCollection,
allowSearch,
orderBy: propOrderBy,
isDesc,
onChange
} = props;

const [isLoading, setIsLoading] = React.useState<boolean>(true);
const [selectedSites, setSelectedSites] = React.useState<ISite[]>();
const [allSites, setAllSites] = React.useState<ISite[]>();
const [filteredSites, setFilteredSites] = React.useState<ISite[]>();
const [searchQuery, setSearchQuery] = React.useState<string>();

const getOptions = (): IDropdownOption[] => {
const result: IDropdownOption[] = [];

if (allowSearch) {
result.push({
key: 'search',
text: '',
itemType: SelectableOptionMenuItemType.Header
});
}

const selectedSitesIds: string[] = selectedSites ? selectedSites.map(s => s.id!) : [];

if (filteredSites) {
filteredSites.forEach(s => {
result.push({
key: s.id,
text: s.title,
data: s,
selected: selectedSitesIds.indexOf(s.id) !== -1
});
});
}

return result;
};

React.useEffect(() => {
if (!initialSites) {
return;
}

setSelectedSites(sites => {
if (!sites) { // we want to set the state one time only
return initialSites;
}

return sites;
});
}, [initialSites]);

React.useEffect(() => {
if (!context) {
return;
}

setIsLoading(true);
setSearchQuery('');
setFilteredSites([]);

let promise: Promise<ISite[]>;
if (mode === 'hub') {
promise = getHubSites(context);
}
else {
promise = getAllSites(context, mode === 'web', limitToCurrentSiteCollection);
}

promise.then(sites => {
const copy = orderBy(sites, [propOrderBy || 'title'], [isDesc ? 'desc' : 'asc']);
setAllSites(copy);
setIsLoading(false);
});
}, [context, mode, limitToCurrentSiteCollection]);

React.useEffect(() => {
setAllSites(sites => {
if (!sites) {
return sites;
}

const copy = orderBy(sites, [propOrderBy || 'title'], [isDesc ? 'desc' : 'asc']);
return copy;
});
}, [propOrderBy, isDesc]);

React.useEffect(() => {
if (!allSites) {
return;
}
setFilteredSites([...allSites]);
}, [allSites]);

if (isLoading) {
return <div className={styles.loadingSpinnerContainer}>
<Spinner size={SpinnerSize.medium} />
</div>;
}

return (
<>
<Dropdown
label={label}
options={getOptions()}
disabled={disabled}
multiSelect={multiSelect !== false}
/>
</>
);
};
93 changes: 93 additions & 0 deletions src/services/SPSitesService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { BaseComponentContext } from '@microsoft/sp-component-base';
import { ISite } from '../controls/sitePicker/ISitePicker';
import { SPHttpClient } from '@microsoft/sp-http';

const getAllSitesInternal = async (ctx: BaseComponentContext, queryText: string): Promise<ISite[]> => {
let startRow = 0;
let rowLimit = 500;
let totalRows = 0;
const values: any[] = [];

//
// getting all sites
//
do {
let userRequestUrl: string = `${ctx.pageContext.web.absoluteUrl}/_api/search/query?querytext='${queryText}'&selectproperties='SiteId,SiteID,WebId,DepartmentId,Title,Path'&rowlimit=${rowLimit}&startrow=${startRow}`;
let searchResponse = await ctx.spHttpClient.get(userRequestUrl, SPHttpClient.configurations.v1);
let sitesResponse = await searchResponse.json();
let relevantResults = sitesResponse.PrimaryQueryResult.RelevantResults;

values.push(...relevantResults.Table.Rows);
totalRows = relevantResults.TotalRows;
startRow += rowLimit;

} while (values.length < totalRows);

// Do the call against the SP REST API search endpoint

let res: ISite[] = [];
res = values.map(element => {
const site: ISite = {} as ISite;
element.Cells.forEach(cell => {
switch (cell.Key) {
case 'Title':
site.title = cell.Value;
break;
case 'Path':
site.url = cell.Value;
break;
case 'SiteId':
case 'SiteID':
site.id = cell.Value;
break;
case 'WebId':
site.webId = cell.Value;
break;
case 'DepartmentId':
if (cell.Value) {
if (cell.Value.indexOf('{') === 0) {
site.hubSiteId = cell.Value.slice(1, -1);
}
else {
site.hubSiteId = cell.Value;
}
}
break;
}
});

return site;
});
return res;
};

export const getAllSites = async (ctx: BaseComponentContext, includeWebs: boolean, currentSiteCollectionOnly: boolean): Promise<ISite[]> => {

let rootUrl: string = ctx.pageContext.web.absoluteUrl;
if (ctx.pageContext.web.serverRelativeUrl !== '/' && (!includeWebs || !currentSiteCollectionOnly)) {
rootUrl = ctx.pageContext.web.absoluteUrl.replace(ctx.pageContext.web.serverRelativeUrl, '');
}

const queryText = `contentclass:STS_Site${includeWebs ? ' contentclass:STS_Web' : ''} Path:${rootUrl}*`;

return getAllSitesInternal(ctx, queryText);
};

export const getHubSites = async (ctx: BaseComponentContext): Promise<ISite[]> => {
const hubSites: ISite[] = [];

const requestUrl = `${ctx.pageContext.site.absoluteUrl}/_api/HubSites?$select=SiteId,ID,SiteUrl,Title`;
const response = await ctx.spHttpClient.get(requestUrl, SPHttpClient.configurations.v1);
const json = await response.json();

json.value.forEach(v => {
hubSites.push({
title: v.Title,
id: v.SiteId,
hubSiteId: v.ID,
url: v.SiteUrl
});
});

return hubSites;
};
10 changes: 10 additions & 0 deletions src/webparts/controlsTest/components/ControlsTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ import {
IControlsTestState
} from "./IControlsTestProps";
import { DragDropFiles } from "../../../DragDropFiles";
import { SitePicker } from "../../../controls/sitePicker/SitePicker";

// Used to render document card
/**
Expand Down Expand Up @@ -1310,6 +1311,15 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC
<FileTypeIcon type={IconType.image} size={this.state.imgSize} />
</div>

<div className="ms-font-m">Site picker tester:
<SitePicker
context={this.props.context}
label={'select sites'}
mode={'web'}
allowSearch={true}
onChange={() => {}} />
</div>

<div className="ms-font-m">List picker tester:
<ListPicker context={this.props.context}
label="Select your list(s)"
Expand Down

0 comments on commit febe234

Please sign in to comment.