Skip to content

Commit febe234

Browse files
committed
basic UI
1 parent e59fb52 commit febe234

File tree

4 files changed

+319
-0
lines changed

4 files changed

+319
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { BaseComponentContext } from '@microsoft/sp-component-base';
2+
3+
export interface ISite {
4+
/**
5+
* ID of the site
6+
*/
7+
id?: string;
8+
/**
9+
* Title
10+
*/
11+
title?: string;
12+
/**
13+
* Base URL
14+
*/
15+
url?: string;
16+
17+
/**
18+
* ID of the web
19+
*/
20+
webId?: string;
21+
22+
/**
23+
* ID of the hub site
24+
*/
25+
hubSiteId?: string;
26+
}
27+
28+
export interface ISitePickerProps {
29+
/**
30+
* Site picker label
31+
*/
32+
label?: string;
33+
/**
34+
* Specify if the control needs to be disabled
35+
*/
36+
disabled?: boolean;
37+
/**
38+
* Web Part context
39+
*/
40+
context: BaseComponentContext;
41+
/**
42+
* Intial data to load in the 'Selected sites' area (optional)
43+
*/
44+
initialSites?: ISite[];
45+
/**
46+
* Define if you want to allow multi site selection. True by default.
47+
*/
48+
multiSelect?: boolean;
49+
/**
50+
* Defines what entities are available for selection: site collections, sites, hub sites.
51+
*/
52+
mode?: 'site' | 'web' | 'hub';
53+
54+
/**
55+
* Specifies if the options should be limited by the current site collections. Taken into consideration if selectionMode is set to 'web'
56+
*/
57+
limitToCurrentSiteCollection?: boolean;
58+
59+
/**
60+
* Specifies if search box is displayed for the component. Default: true
61+
*/
62+
allowSearch?: boolean;
63+
64+
/**
65+
* Specifices if the list is sorted by title or url. Default: title
66+
*/
67+
orderBy?: 'title' | 'url';
68+
69+
/**
70+
* Specifies if the list is sorted in descending order. Default: false
71+
*/
72+
isDesc?: boolean;
73+
74+
/**
75+
* selection change handler
76+
*/
77+
onChange: (selectedSites: ISite[]) => void;
78+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import * as React from 'react';
2+
import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';
3+
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
4+
import { ISite, ISitePickerProps } from './ISitePicker';
5+
import { getAllSites, getHubSites } from '../../services/SPSitesService';
6+
import { IDropdownOption, Dropdown } from 'office-ui-fabric-react/lib/Dropdown';
7+
import { SelectableOptionMenuItemType } from 'office-ui-fabric-react/lib/utilities/selectableOption/SelectableOption.types';
8+
import orderBy from 'lodash/orderBy';
9+
10+
const styles = mergeStyleSets({
11+
loadingSpinnerContainer: {
12+
width: '100%',
13+
textAlign: 'center'
14+
}
15+
});
16+
17+
export const SitePicker: React.FunctionComponent<ISitePickerProps> = (props: React.PropsWithChildren<ISitePickerProps>) => {
18+
19+
const {
20+
label,
21+
disabled,
22+
context,
23+
initialSites,
24+
multiSelect,
25+
mode,
26+
limitToCurrentSiteCollection,
27+
allowSearch,
28+
orderBy: propOrderBy,
29+
isDesc,
30+
onChange
31+
} = props;
32+
33+
const [isLoading, setIsLoading] = React.useState<boolean>(true);
34+
const [selectedSites, setSelectedSites] = React.useState<ISite[]>();
35+
const [allSites, setAllSites] = React.useState<ISite[]>();
36+
const [filteredSites, setFilteredSites] = React.useState<ISite[]>();
37+
const [searchQuery, setSearchQuery] = React.useState<string>();
38+
39+
const getOptions = (): IDropdownOption[] => {
40+
const result: IDropdownOption[] = [];
41+
42+
if (allowSearch) {
43+
result.push({
44+
key: 'search',
45+
text: '',
46+
itemType: SelectableOptionMenuItemType.Header
47+
});
48+
}
49+
50+
const selectedSitesIds: string[] = selectedSites ? selectedSites.map(s => s.id!) : [];
51+
52+
if (filteredSites) {
53+
filteredSites.forEach(s => {
54+
result.push({
55+
key: s.id,
56+
text: s.title,
57+
data: s,
58+
selected: selectedSitesIds.indexOf(s.id) !== -1
59+
});
60+
});
61+
}
62+
63+
return result;
64+
};
65+
66+
React.useEffect(() => {
67+
if (!initialSites) {
68+
return;
69+
}
70+
71+
setSelectedSites(sites => {
72+
if (!sites) { // we want to set the state one time only
73+
return initialSites;
74+
}
75+
76+
return sites;
77+
});
78+
}, [initialSites]);
79+
80+
React.useEffect(() => {
81+
if (!context) {
82+
return;
83+
}
84+
85+
setIsLoading(true);
86+
setSearchQuery('');
87+
setFilteredSites([]);
88+
89+
let promise: Promise<ISite[]>;
90+
if (mode === 'hub') {
91+
promise = getHubSites(context);
92+
}
93+
else {
94+
promise = getAllSites(context, mode === 'web', limitToCurrentSiteCollection);
95+
}
96+
97+
promise.then(sites => {
98+
const copy = orderBy(sites, [propOrderBy || 'title'], [isDesc ? 'desc' : 'asc']);
99+
setAllSites(copy);
100+
setIsLoading(false);
101+
});
102+
}, [context, mode, limitToCurrentSiteCollection]);
103+
104+
React.useEffect(() => {
105+
setAllSites(sites => {
106+
if (!sites) {
107+
return sites;
108+
}
109+
110+
const copy = orderBy(sites, [propOrderBy || 'title'], [isDesc ? 'desc' : 'asc']);
111+
return copy;
112+
});
113+
}, [propOrderBy, isDesc]);
114+
115+
React.useEffect(() => {
116+
if (!allSites) {
117+
return;
118+
}
119+
setFilteredSites([...allSites]);
120+
}, [allSites]);
121+
122+
if (isLoading) {
123+
return <div className={styles.loadingSpinnerContainer}>
124+
<Spinner size={SpinnerSize.medium} />
125+
</div>;
126+
}
127+
128+
return (
129+
<>
130+
<Dropdown
131+
label={label}
132+
options={getOptions()}
133+
disabled={disabled}
134+
multiSelect={multiSelect !== false}
135+
/>
136+
</>
137+
);
138+
};

src/services/SPSitesService.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { BaseComponentContext } from '@microsoft/sp-component-base';
2+
import { ISite } from '../controls/sitePicker/ISitePicker';
3+
import { SPHttpClient } from '@microsoft/sp-http';
4+
5+
const getAllSitesInternal = async (ctx: BaseComponentContext, queryText: string): Promise<ISite[]> => {
6+
let startRow = 0;
7+
let rowLimit = 500;
8+
let totalRows = 0;
9+
const values: any[] = [];
10+
11+
//
12+
// getting all sites
13+
//
14+
do {
15+
let userRequestUrl: string = `${ctx.pageContext.web.absoluteUrl}/_api/search/query?querytext='${queryText}'&selectproperties='SiteId,SiteID,WebId,DepartmentId,Title,Path'&rowlimit=${rowLimit}&startrow=${startRow}`;
16+
let searchResponse = await ctx.spHttpClient.get(userRequestUrl, SPHttpClient.configurations.v1);
17+
let sitesResponse = await searchResponse.json();
18+
let relevantResults = sitesResponse.PrimaryQueryResult.RelevantResults;
19+
20+
values.push(...relevantResults.Table.Rows);
21+
totalRows = relevantResults.TotalRows;
22+
startRow += rowLimit;
23+
24+
} while (values.length < totalRows);
25+
26+
// Do the call against the SP REST API search endpoint
27+
28+
let res: ISite[] = [];
29+
res = values.map(element => {
30+
const site: ISite = {} as ISite;
31+
element.Cells.forEach(cell => {
32+
switch (cell.Key) {
33+
case 'Title':
34+
site.title = cell.Value;
35+
break;
36+
case 'Path':
37+
site.url = cell.Value;
38+
break;
39+
case 'SiteId':
40+
case 'SiteID':
41+
site.id = cell.Value;
42+
break;
43+
case 'WebId':
44+
site.webId = cell.Value;
45+
break;
46+
case 'DepartmentId':
47+
if (cell.Value) {
48+
if (cell.Value.indexOf('{') === 0) {
49+
site.hubSiteId = cell.Value.slice(1, -1);
50+
}
51+
else {
52+
site.hubSiteId = cell.Value;
53+
}
54+
}
55+
break;
56+
}
57+
});
58+
59+
return site;
60+
});
61+
return res;
62+
};
63+
64+
export const getAllSites = async (ctx: BaseComponentContext, includeWebs: boolean, currentSiteCollectionOnly: boolean): Promise<ISite[]> => {
65+
66+
let rootUrl: string = ctx.pageContext.web.absoluteUrl;
67+
if (ctx.pageContext.web.serverRelativeUrl !== '/' && (!includeWebs || !currentSiteCollectionOnly)) {
68+
rootUrl = ctx.pageContext.web.absoluteUrl.replace(ctx.pageContext.web.serverRelativeUrl, '');
69+
}
70+
71+
const queryText = `contentclass:STS_Site${includeWebs ? ' contentclass:STS_Web' : ''} Path:${rootUrl}*`;
72+
73+
return getAllSitesInternal(ctx, queryText);
74+
};
75+
76+
export const getHubSites = async (ctx: BaseComponentContext): Promise<ISite[]> => {
77+
const hubSites: ISite[] = [];
78+
79+
const requestUrl = `${ctx.pageContext.site.absoluteUrl}/_api/HubSites?$select=SiteId,ID,SiteUrl,Title`;
80+
const response = await ctx.spHttpClient.get(requestUrl, SPHttpClient.configurations.v1);
81+
const json = await response.json();
82+
83+
json.value.forEach(v => {
84+
hubSites.push({
85+
title: v.Title,
86+
id: v.SiteId,
87+
hubSiteId: v.ID,
88+
url: v.SiteUrl
89+
});
90+
});
91+
92+
return hubSites;
93+
};

src/webparts/controlsTest/components/ControlsTest.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ import {
163163
IControlsTestState
164164
} from "./IControlsTestProps";
165165
import { DragDropFiles } from "../../../DragDropFiles";
166+
import { SitePicker } from "../../../controls/sitePicker/SitePicker";
166167

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

1314+
<div className="ms-font-m">Site picker tester:
1315+
<SitePicker
1316+
context={this.props.context}
1317+
label={'select sites'}
1318+
mode={'web'}
1319+
allowSearch={true}
1320+
onChange={() => {}} />
1321+
</div>
1322+
13131323
<div className="ms-font-m">List picker tester:
13141324
<ListPicker context={this.props.context}
13151325
label="Select your list(s)"

0 commit comments

Comments
 (0)