React image gallery (#799)

* react-image-gallery web part added

* Update README.md
This commit is contained in:
Ejaz Hussain 2019-03-09 10:41:07 +00:00 committed by Vesa Juvonen
parent b5331ed08d
commit 2194dd957b
33 changed files with 19167 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

32
samples/react-image-gallery/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,11 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.6.0",
"libraryName": "image-gallery",
"libraryId": "876cbbe8-6974-4676-9da3-5c190ac8164f",
"packageManager": "npm",
"componentType": "webpart"
}
}

View File

@ -0,0 +1,62 @@
# Filterable Image Gallery Web Part
## Summary
This sample describe a SPFX application which implement an image gallery with taxonomy base filtering and typed search. This application also implement pagination.
![Filterable Image Gallery web part built on the SharePoint Framework using React](./assets/image-gallery.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
> Update accordingly as needed.
## Prerequisites
> Any special pre-requisites?
## Solution
Solution|Author(s)
--------|---------
react-image-gallery | Ejaz Hussain
## Version history
Version|Date|Comments
-------|----|--------
1.0|March 01, 2019|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
> Include any additional steps as needed.
- Create a Department Term set with associated child terms, for example, HR, Information Services, Sales, Marketing
- Create an Image Library and add some sample images
- Tag each image with Department Metadata Column
- Also fill in Title field for each image, this is require for typed search functionality
## Features
Here are main features for this application
- Taxonomy based filtering
- Typed Search
- Right side popup panel
- Server side pagination using REST API
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/readme-template" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"image-gallery-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/imageGallery/ImageGalleryWebPart.js",
"manifest": "./src/webparts/imageGallery/ImageGalleryWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ImageGalleryWebPartStrings": "lib/webparts/imageGallery/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "image-gallery",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "image-gallery-client-side-solution",
"id": "876cbbe8-6974-4676-9da3-5c190ac8164f",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/image-gallery.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,7 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"name": "react-image-gallery",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.6.0",
"@microsoft/sp-lodash-subset": "1.6.0",
"@microsoft/sp-office-ui-fabric-core": "1.6.0",
"@microsoft/sp-webpart-base": "1.6.0",
"@pnp/common": "^1.2.7",
"@pnp/graph": "^1.2.7",
"@pnp/logging": "^1.2.7",
"@pnp/odata": "^1.2.7",
"@pnp/sp": "^1.2.7",
"@pnp/spfx-controls-react": "1.11.0",
"@types/es6-promise": "0.0.33",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@types/webpack-env": "1.13.1",
"react": "15.6.2",
"react-dom": "15.6.2",
"react-js-pagination": "^3.0.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.6.0",
"@microsoft/sp-module-interfaces": "1.6.0",
"@microsoft/sp-webpart-workbench": "1.6.0",
"tslint-microsoft-contrib": "~5.0.0",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,8 @@
/**
* Determines the display mode of the given control or form.
*/
export enum ControlMode {
Display = 1,
Edit = 2,
New = 3,
}

View File

@ -0,0 +1,3 @@
export default class Constants {
public static readonly taxListColumnName = "Department";
}

View File

@ -0,0 +1,7 @@
export interface IImage {
Id: string;
Title: string;
Department: string[];
FileRef: string;
FileLeafRef?:string;
}

View File

@ -0,0 +1,5 @@
export interface IListService {
getListItemsCount:(url:string)=>Promise<any>;
readItems:(url:string)=>Promise<any>;
}

View File

@ -0,0 +1,3 @@
export { IListService } from "./IListService";
export { IImage } from './IImage';

View File

@ -0,0 +1,147 @@
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http';
import { reject } from 'lodash';
import { IListService, IImage } from '../Interfaces';
import { sp, spODataEntityArray, Item } from "@pnp/sp";
import Constants from '../Common/constants';
import { stringIsNullOrEmpty } from '@pnp/common';
export class ListService implements IListService {
private spHttpClient: SPHttpClient;
constructor(spHttpClient?: SPHttpClient) {
this.spHttpClient = spHttpClient;
}
public async readItems(url: string): Promise<any> {
try {
const response = await this.spHttpClient.get(url, SPHttpClient.configurations.v1,
{
headers: {
'Accept': 'application/json;odata=nometadata',
'odata-version': ''
}
});
const items: any = await response.json();
let result = {};
if (items.value.length) {
result = {
items: items.value,
nextLink: items["odata.nextLink"]
}
}
else {
result = null;
}
return result;
}
catch (error) {
return error;
}
// return new Promise<any>(async (resolve) => {
// this.spHttpClient.get(url, SPHttpClient.configurations.v1,
// {
// headers: {
// 'Accept': 'application/json;odata=nometadata',
// 'odata-version': ''
// }
// }).then((response: SPHttpClientResponse): Promise<{ value: number }> => {
// return response.json();
// }).then((response: { value: number }): void => {
// resolve(response.value);
// });
// });
}
public async getListItemsCount(url: string): Promise<any> {
try {
const response = await this.spHttpClient.get(url, SPHttpClient.configurations.v1,
{
headers: {
'Accept': 'application/json;odata=nometadata',
'odata-version': ''
}
});
const result: any = await response.json();
return result.value;
}
catch (error) {
return error;
}
// return new Promise<any>(async (resolve) => {
// this.spHttpClient.get(url, SPHttpClient.configurations.v1,
// {
// headers: {
// 'Accept': 'application/json;odata=nometadata',
// 'odata-version': ''
// }
// }).then((response: SPHttpClientResponse): Promise<{ value: number }> => {
// return response.json();
// }).then((response: { value: number }): void => {
// resolve(response.value);
// });
// });
}
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,26 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "40d24cef-64e6-4367-9ca4-5b2946e8185d",
"alias": "ImageGalleryWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "imageGallery" },
"description": { "default": "imageGallery description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "imageGallery"
}
}]
}

View File

@ -0,0 +1,117 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneSlider
} from '@microsoft/sp-webpart-base';
import * as strings from 'ImageGalleryWebPartStrings';
import ImageGallery from './components/ImageGallery';
import { IImageGalleryProps } from './components/IImageGalleryProps';
import ConfigureWebPart from './components/ConfigureWebPart/ConfigureWebPart';
import { sp } from '@pnp/sp';
import { ListService } from '../../Services/ListService';
export interface IImageGalleryWebPartProps {
imageLibrary: string;
pageSize: number;
}
export default class ImageGalleryWebPart extends BaseClientSideWebPart<IImageGalleryWebPartProps> {
private listService: ListService
protected async onInit(): Promise<void> {
const _ = await super.onInit();
this.listService = new ListService(this.context.spHttpClient);
sp.setup({
spfxContext: this.context
});
}
public render(): void {
// const element: React.ReactElement<IImageGalleryProps > = React.createElement(
// ImageGallery,
// {
// description: this.properties.imageLibrary
// }
// );
let element: any;
if (this.properties.imageLibrary && this.properties.pageSize) {
element = React.createElement<IImageGalleryProps>(
ImageGallery,
{
listName: this.properties.imageLibrary,
context: this.context,
siteUrl: this.context.pageContext.site.absoluteUrl,
pageSize: this.properties.pageSize
}
);
}
else {
// show configure web part react component
element = React.createElement(
ConfigureWebPart,
{
webPartContext: this.context,
title: "Image Gallery",
description: strings.MissingListConfiguration,
buttonText: strings.ConfigureWebpartButtonText
}
);
}
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('imageLibrary', {
label: strings.ImageLibraryFieldLabel
}),
PropertyPaneSlider('pageSize', {
label: "Page Size",
min: 2,
max: 20,
value: 5,
showValue: true,
step: 1
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,16 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.configureWebPart {
.container {
.title {
font-size: 18px;
}
.description {
margin-top: 20px;
margin-bottom: 20px;
}
.button {
margin-bottom: 20px;
}
}
}

View File

@ -0,0 +1,43 @@
import * as React from 'react';
import { IWebPartContext } from '@microsoft/sp-webpart-base';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import styles from './ConfigureWebPart.module.scss';
export interface IConfigureWebPartProps {
webPartContext: IWebPartContext;
title: string;
description?: string;
buttonText?: string;
}
const ConfigureWebPart: React.SFC<IConfigureWebPartProps> = (props) => {
const {
webPartContext,
title,
description,
buttonText,
} = props;
return (
<div className={styles.container}>
<div className={styles.title}>{title}</div>
<div className={styles.description}>
<MessageBar messageBarType={MessageBarType.info} >
{description ? description : 'Please configure this web part\'s properties first.'}
</MessageBar>
</div>
<div className={styles.button}>
<PrimaryButton iconProps={{ iconName: 'Edit' }} onClick={(e) => { e.preventDefault(); webPartContext.propertyPane.open(); }}>
{buttonText ? buttonText : 'Configure Web Part'}
</PrimaryButton>
</div>
</div>
);
};
export default ConfigureWebPart;

View File

@ -0,0 +1,6 @@
export interface IImageGalleryProps {
listName: string;
context: any;
siteUrl:string;
pageSize:number;
}

View File

@ -0,0 +1,18 @@
import { IImage } from "../../../Interfaces";
export interface IImageGalleryState {
showPanel: boolean;
selectedImage?: IImage;
showLoader: boolean;
itemsNotFoundMessage?: string,
sQuery?: string,
dQuery?: string
itemsNotFound?: boolean,
itemCount?: number;
pageSize?: number;
currentPage?: number;
items?: any[];
status?: string;
nextLink: string;
}

View File

@ -0,0 +1,182 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
// Make workbench canvas use full page width
:global(#workbenchPageContent) {
max-width: initial;
}
:global(.ControlZone) {
max-width: initial;
}
:global(.ms-TextField-fieldGroup) {
height: 34px;
}
.imageGallery {
.container {
@include ms-Grid;
margin-right: auto;
margin-left: auto;
}
.row {
@include ms-Grid-row;
}
.column {
@include ms-Grid-col;
}
.mslg12 {
@include ms-lg12;
}
.mslg6 {
@include ms-lg6;
}
.mslg3 {
@include ms-lg3;
}
.header {
.pageTitle {
padding-bottom: 9px;
margin: 40px 0 20px;
border-bottom: 1px solid #eee;
}
small {
font-size: 65%;
font-weight: 400;
line-height: 1;
color: #777;
}
}
.filters {
.filter {
padding-top: 0px;
padding-bottom: 15px;
}
.searchBox {
padding-top: 0px;
padding-bottom: 15px;
.searchBoxInputField {
padding: 0px;
}
}
}
.mainContent {
.thumbnail {
cursor: pointer;
display: block;
padding: 4px;
margin-bottom: 20px;
line-height: 1.42857143;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
-webkit-transition: border .2s ease-in-out;
-o-transition: border .2s ease-in-out;
transition: border .2s ease-in-out;
img {
margin-right: auto;
margin-left: auto;
display: block;
max-width: 100%;
height: auto;
border: 0px;
}
}
.modalContent {
box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
position: relative;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, .2);
border-radius: 6px;
outline: 0;
.modalBody {
position: relative;
padding: 15px;
}
}
}
.pagination {
.pager {
padding-left: 0;
margin: 20px 0;
text-align: center;
list-style: none;
li {
display: inline;
padding: 0px 10px;
button {
display: inline-block;
padding: 5px 14px;
background-color: $ms-color-themePrimary;
border: 1px solid #ddd;
border-radius: 15px;
color: #fff;
text-decoration: none;
&:disabled {
background-color: $ms-color-neutralTertiary;
}
}
}
}
}
.loader {
margin: 0 auto;
.circle {
width: 70px;
height: 70px;
}
}
.customIcons {
.msIconTag {
font-size: 18px;
vertical-align: middle;
}
}
.hidden {
display: none !important;
}
.visible {
visibility: visible !important;
}
.status{
float:left
}
figcaption{
background-color: #eee;
padding: 5px 0px;
text-align: center;
text-decoration: none;
}
}
//common
.panel {
border-color: #ddd;
margin-bottom: 20px;
background-color: #f8f8f8;
border: 1px solid #ddd;
border-radius: 4px;
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
box-sizing: border-box;
.panelBody {
padding: 15px 0px;
}
}
.listGroup {
display: flex;
flex-direction: column;
padding-left: 0;
margin-bottom: 0;
max-width: 400px;
margin-top: 0;
.listGroupItem {
position: relative;
display: block;
padding: .75rem 1.25rem;
margin-bottom: -1px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, .125);
}
}

View File

@ -0,0 +1,403 @@
import * as React from 'react';
import styles from './ImageGallery.module.scss';
import { IImageGalleryProps } from './IImageGalleryProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { css, classNamesFunction, IStyleFunction } from '@uifabric/utilities/lib';
import { TaxonomyPicker, IPickerTerms } from "@pnp/spfx-controls-react/lib/TaxonomyPicker";
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { IListService, IImage } from '../../../Interfaces';
import { ListService } from '../../../Services/ListService';
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
import { objectDefinedNotNull, stringIsNullOrEmpty } from '@pnp/common';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { Button } from 'office-ui-fabric-react/lib/Button';
import { IImageGalleryState } from './IImageGalleryState';
export default class ImageGallery extends React.Component<IImageGalleryProps, IImageGalleryState> {
private _spService: IListService;
private selectQuery: string[] = [];
private expandQuery: string[] = [];
private filterQuery: string[] = [];
private urlCollection: string[] = [];
/**
*
*/
constructor(props: IImageGalleryProps, state: IImageGalleryState) {
super(props);
this.state = {
items: [],
showPanel: false,
selectedImage: {} as IImage,
showLoader: false,
itemsNotFound: false,
pageSize: this.props.pageSize,
currentPage: 1,
nextLink: "",
}
this._onTaxPickerChange = this._onTaxPickerChange.bind(this);
this._onClickNext = this._onClickNext.bind(this);
this._onClickPrevious = this._onClickPrevious.bind(this);
this._onImageClick = this._onImageClick.bind(this);
this._onSearchChange = this._onSearchChange.bind(this);
this._spService = new ListService(this.props.context.spHttpClient);
}
public async componentDidMount() {
//Get Images from the library
let value = await this._spService.getListItemsCount(`${this.props.siteUrl}/_api/web/lists/GetByTitle('${this.props.listName}')/ItemCount`);
this.setState({
itemCount: value
});
const queryParam = this.buildQueryParams();
let url = `${this.props.siteUrl}/_api/web/lists/GetByTitle('${this.props.listName}')/items/${queryParam}`;
this.urlCollection.push(url);
this._readItems(url);
}
private async _readItems(url: string) {
this.setState({
items: [],
status: 'Loading all items...',
showLoader: true
});
let response = await this._spService.readItems(url);
if (objectDefinedNotNull(response)) {
if (objectDefinedNotNull(response.nextLink)) {
this.urlCollection.push(response.nextLink);
}
this.setState({
showLoader: false,
items: response.items,
status: `Showing items ${(this.state.currentPage - 1) * this.props.pageSize + 1} - ${((this.state.currentPage - 1) * this.props.pageSize) + response.items.length} of ${this.state.itemCount}`
});
}
else {
this.setState({
showLoader: false,
items: [],
status: "",
itemsNotFound: true
});
}
}
private async _onClickNext() {
this.setState({
currentPage: this.state.currentPage + 1,
showLoader: true
});
let url = this.urlCollection[this.urlCollection.length - 1];
this._readItems(url);
}
private async _onClickPrevious() {
let url = "";
if (this.urlCollection.length > 1) {
this.setState({
currentPage: this.state.currentPage - 1
})
this.urlCollection.pop();
url = this.urlCollection[this.urlCollection.length - 1];
}
else {
url = this.urlCollection[0];
}
this.setState({
items: [],
status: 'Loading all items...',
showLoader: true
});
let response = await this._spService.readItems(url);
this.setState({
showLoader: false,
items: response.items,
status: `Showing items ${(this.state.currentPage - 1) * this.props.pageSize + 1} - ${((this.state.currentPage - 1) * this.props.pageSize) + response.items.length} of ${this.state.itemCount}`
});
}
private buildQueryParams(taxQuery?: string, searchQuery?: string): string {
this.selectQuery = [];
this.expandQuery = [];
this.filterQuery = [];
this.selectQuery.push("ID");
this.selectQuery.push("Title");
this.selectQuery.push("FileRef");
this.selectQuery.push("FileLeafRef");
this.selectQuery.push("Department");
this.selectQuery.push("TaxCatchAll/Term");
this.expandQuery.push("TaxCatchAll");
this.filterQuery.push(this.buildFilterQuery(taxQuery, searchQuery));
const queryParam = `?%24skiptoken=Paged%3dTRUE%26p_ID=0&$top=${this.state.pageSize}`;
const selectColumns = this.selectQuery === null || this.selectQuery === undefined || this.selectQuery.length === 0 ? "" : '&$select=' + this.selectQuery.join();
const filterColumns = this.filterQuery === null || this.filterQuery === undefined || this.filterQuery.length === 0 ? "" : '&$filter=' + this.filterQuery.join();
const expandColumns = this.expandQuery === null || this.expandQuery === undefined || this.expandQuery.length === 0 ? "" : '&$expand=' + this.expandQuery.join();
return queryParam + selectColumns + filterColumns + expandColumns;
}
private buildQueryParamsTotalFilteredItems(taxQuery?: string, searchQuery?: string): string {
this.selectQuery = [];
this.filterQuery = [];
this.selectQuery.push("ID");
this.filterQuery.push(this.buildFilterQuery(taxQuery, searchQuery));
const queryParam = "?";
const selectColumns = this.selectQuery === null || this.selectQuery === undefined || this.selectQuery.length === 0 ? "" : '&$select=' + this.selectQuery.join();
const filterColumns = this.filterQuery === null || this.filterQuery === undefined || this.filterQuery.length === 0 ? "" : '&$filter=' + this.filterQuery.join();
return queryParam + selectColumns + filterColumns;
}
private buildFilterQuery(taxQuery: string, searchQuery: string) {
let result: string = "";
if (!stringIsNullOrEmpty(taxQuery) && stringIsNullOrEmpty(searchQuery)) {
result = `TaxCatchAll/Term eq '${taxQuery}'`;
}
if (stringIsNullOrEmpty(taxQuery) && !stringIsNullOrEmpty(searchQuery)) {
result = `startswith(Title,'${searchQuery}')`;
}
if (!stringIsNullOrEmpty(taxQuery) && !stringIsNullOrEmpty(searchQuery)) {
result = `(TaxCatchAll/Term eq '${taxQuery}') and (startswith(Title,'${searchQuery}'))`;
}
if (stringIsNullOrEmpty(taxQuery) && stringIsNullOrEmpty(searchQuery)) {
result = "";
}
return result;
}
private async _onTaxPickerChange(terms: IPickerTerms) {
this.urlCollection = [];
let query = "";
query = terms.length && terms[0].name ? terms[0].name : "";
this.setState({
dQuery: query
});
let queryParam = this.buildQueryParamsTotalFilteredItems(query, this.state.sQuery);
let response = await this._spService.readItems(`${this.props.siteUrl}/_api/web/lists/GetByTitle('${this.props.listName}')/items/${queryParam}`);
this.setState({
itemCount: response.items.length
});
queryParam = this.buildQueryParams(query, this.state.sQuery);
let url = `${this.props.siteUrl}/_api/web/lists/GetByTitle('${this.props.listName}')/items/${queryParam}`;
this.urlCollection.push(url);
this._readItems(url);
}
private async _onSearchChange(query: any) {
this.urlCollection = [];
this.setState({
sQuery: query
});
let queryParam = this.buildQueryParamsTotalFilteredItems(this.state.dQuery, query);
let response = await this._spService.readItems(`${this.props.siteUrl}/_api/web/lists/GetByTitle('${this.props.listName}')/items/${queryParam}`);
if (objectDefinedNotNull(response)) {
this.setState({
itemCount: response.items.length
});
}
else {
this.setState({
itemCount: 0
});
}
queryParam = this.buildQueryParams(this.state.dQuery, query);
let url = `${this.props.siteUrl}/_api/web/lists/GetByTitle('${this.props.listName}')/items/${queryParam}`;
this.urlCollection.push(url);
this._readItems(url);
}
private _onImageClick(selectedImage: any): void {
this.setState({
selectedImage,
showPanel: true
});
}
public render(): React.ReactElement<IImageGalleryProps> {
const spinnerStyles = props => ({
circle: [
{
width: '60px',
height: '60px',
borderWidth: '4px',
selectors: {
':hover': {
background: 'f8f8ff8',
}
}
}
]
});
let result = [];
let tagList;
if (this.state.items.length) {
result = this.state.items.map((item, index) => {
return (
<div key={index} className={css(styles.column, styles.mslg3)} onClick={() => this._onImageClick(item)}>
<div className={css(styles.thumbnail)}>
<img src={item.FileRef} title={item.Title} id={item.Id} />
<figcaption>{item.Title}</figcaption>
</div>
</div>
)
});
}
if (objectDefinedNotNull(this.state.selectedImage.Department)) {
tagList = this.state.selectedImage.Department.map((tag: any, index) => {
return <li className={styles.listGroupItem} key={index}> <Icon iconName="Tag" className={styles.msIconTag} /> {tag.Label}</li>;
});
}
return (
<div className={styles.imageGallery}>
<div className={styles.container} dir="ltr">
<div className={css(styles.row, styles.header)}>
<div className={css(styles.column, styles.mslg12, styles.pageTitle)}>
<h1>Image Gallery <small> Filterable</small></h1></div>
</div>
<div className={css(styles.row, styles.filters)}>
<div className={css(styles.column, styles.mslg12, styles.panel)}>
<div className={styles.panelBody}>
<div className={css(styles.column, styles.mslg3, styles.filter)}>
<TaxonomyPicker
allowMultipleSelections={false}
termsetNameOrID="Departments"
panelTitle="Select Term"
label="Filter by department"
context={this.props.context}
onChange={this._onTaxPickerChange}
isTermSetSelectable={false}
/>
</div>
<div className={css(styles.column, styles.mslg3, "ms-u-lgPush6", styles.searchBox)}>
<TextField label="Search" className={styles.searchBoxInputField} placeholder="Enter search term" onChanged={this._onSearchChange} />
</div>
</div>
</div>
</div>
<div className={css(styles.row)}>
<div className={css(styles.column, styles.mslg12, styles.panel)}>
<div className={styles.panelBody}>
{
this.state.showLoader
? <Spinner size={SpinnerSize.large} label="loading..." className={css(styles.loader)} getStyles={spinnerStyles} />
: ""
}
<div className={css(styles.row, styles.mainContent)}>
{result.length > 0 ? result : ""}
{!result.length && this.state.itemsNotFound ? <MessageBar
messageBarType={MessageBarType.warning}
isMultiline={false}
// onDismiss={log('test')}
dismissButtonAriaLabel="Close"
>
Items not found. Try different search keyword
</MessageBar> : ""}
<Panel
isOpen={this.state.showPanel}
type={PanelType.medium}
onDismiss={() => this.setState({ showPanel: false })}
headerText={this.state.selectedImage.Title}
>
<div className={styles.modalContent}>
<div className={styles.modalBody}>
<div className={styles.thumbnail}>
<img src={this.state.selectedImage.FileRef} title={this.state.selectedImage.Title} id={this.state.selectedImage.Id} />
</div>
<h3>Tags</h3>
{this.state.selectedImage.Department ?
<ul className={styles.listGroup}>
{
tagList
}
</ul> : ""}
</div>
</div>
</Panel>
</div>
</div>
</div>
</div>
<div className={css(styles.row, styles.pagination)}>
<div className={css(styles.column, styles.mslg12, styles.panel)}>
<div className={styles.panelBody}>
<div className={styles.status}>{this.state.status}</div>
<ul className={styles.pager}>
<li>
<Button disabled={((this.state.currentPage - 1) * this.props.pageSize + 1) <= 1} onClick={this._onClickPrevious}>Previous</Button>
</li>
<li>
<Button disabled={((this.state.currentPage - 1) * this.props.pageSize) + this.state.items.length >= this.state.itemCount} onClick={this._onClickNext}>Next</Button>
</li>
</ul>
</div></div>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,9 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Filterable Gallery",
"ImageLibraryFieldLabel": "Image Library Name",
'MissingListConfiguration': 'Please configure a SharePoint list in the web part\'s properties.',
'ConfigureWebpartButtonText': 'Configure Web Part'
}
});

View File

@ -0,0 +1,12 @@
declare interface IImageGalleryWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
ImageLibraryFieldLabel: string;
MissingListConfiguration: string;
ConfigureWebpartButtonText: string;
}
declare module 'ImageGalleryWebPartStrings' {
const strings: IImageGalleryWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,32 @@
{
"rulesDirectory": [
"tslint-microsoft-contrib"
],
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}