I am using React to develop with SPfx for SharePoint online and am trying to rebuild a past solution I made, so that I can utilize the property pane of sharepoint to edit the webpart. I am getting an error of:
ERROR:
Cannot read properties of undefined (reading 'fileAbsoluteUrl')
when trying to render the grid. However, using the Edge developer tools, I can see the array being initialized with the information from the Property Pane.
If anybody could help explain why the code is returning an undefined, I would greatly appreciate it. Or if you have experience using PnP with SPFx.
This is my .tsx code:
export default class GridLayout extends React.Component<IGridLayoutProps, {}> {
public render(): React.ReactElement<IGridLayoutProps> {
const {
description,
isDarkTheme,
environmentMessage,
hasTeamsContext,
userDisplayName,
gridItems
} = this.props;
let arr = [];
arr.push(this.props.gridItems);
let firstItem = arr.slice(0, 1);
let firstRow = arr.slice(1, 6);
let items = arr.slice(6);
console.log(firstItem, firstRow, items);
if (this.props.gridItems && this.props.gridItems.length > 0) {
var gridFirstItem = firstItem.map(el => {
<div className={`${styles.tile1}`}>
<div key={el.Title} className={`${styles.galleryframe1}`} style={{ backgroundColor: '#2f2f2f' }}>
<a href={el.Hyperlink ? el.Hyperlink : '#'}>
<img className={`${styles.galleryimg}`} src={el && el.length > 0 ? el.filePicker.fileAbsoluteUrl : ''} />
</a>
</div>
</div>
});
console.log('first grid item good...')
var gridFirstRow = firstRow.map(el => {
<div className={`${styles.tile1}`}>
<div key={el.Title} className={`${styles.galleryframe1}`} style={{ backgroundColor: '#0069f8' }}>
<a href={el.Hyperlink ? el.Hyperlink : '#'}>
<img className={`${styles.galleryimg}`} src={el && el.length > 0 ? el.filePicker.fileAbsoluteUrl : ''} />
</a>
</div>
</div>
});
console.log('first row good...')
var restOfItems = items.map(el => {
<div className={`${styles.tile}`}>
<div key={el.Title} className={`${styles.galleryframe}`} style={{ backgroundColor: '#e64856' }}>
<a href={el.Hyperlink ? el.Hyperlink : '#'}>
<img className={`${styles.galleryimg}`} src={el && el.length > 0 ? el.filePicker.fileAbsoluteUrl : ''} />
</a>
</div>
</div>
});
console.log('grid good')
} else {
return (
<div className={`${styles.label}`}>Use Property Pane Editor to Add Tiles.</div>
)
}
return (
<body>
<div className={`${styles.footer}`} >Our Mission: Provide the fullest possible accounting for our missing personnel to their families and the nation.</div>
{/* Renders the grid */}
<div className={`${styles.grid}`}>
{gridFirstItem}
{gridFirstRow}
{restOfItems}
</div>
</body>
);
}
}
This is the code involving the property pane and the array:
PropertyFieldCollectionData("gridItems", {
key: "gridItemsFieldId",
label: "Grid Data",
panelHeader: "Grid Data Panel",
manageBtnLabel: "Manage grid data",
value: this.properties.gridItems,
fields: [
{
id: "Title",
title: "Item Title",
type: CustomCollectionFieldType.string,
required: true,
},
{
id: "Description",
title: "Item Description",
type: CustomCollectionFieldType.string,
},
{
id: "Hyperlink",
title: "Link to Open",
type: CustomCollectionFieldType.url,
required: true,
},
{
id: "filePicker",
title: "Select File",
type: CustomCollectionFieldType.custom,
onCustomRender: (
field,
value,
onUpdate,
item,
itemId,
onError
) => {
return React.createElement(FilePicker, {
context: this.context,
key: itemId,
buttonLabel: "Select File",
onChange: (filePickerResult: IFilePickerResult[]) => {
console.log('changing....', field);
onUpdate(field.id, filePickerResult[0]);
this.context.propertyPane.refresh();
this.render();
},
onSave: (filePickerResult: IFilePickerResult[]) => {
console.log('saving....', field);
if (filePickerResult && filePickerResult.length > 0) {
console.log('filePickerResult && filePickerResult.length > 0');
if (filePickerResult[0].fileAbsoluteUrl == null) {
console.log('filePickerResult[0].fileAbsoluteUrl == null');
filePickerResult[0].downloadFileContent().then(async r => {
let fileresult = await this.web.getFolderByServerRelativeUrl(`${this.context.pageContext.site.serverRelativeUrl}/SiteAssets/SitePages`).files.addUsingPath(filePickerResult[0].fileName, r, true);
filePickerResult[0].fileAbsoluteUrl = `${this.context.pageContext.site.absoluteUrl}/SiteAssets/SitePages/${fileresult.data.Name}`;
console.log('saving....', filePickerResult[0]);
onUpdate(field.id, filePickerResult[0]);
this.context.propertyPane.refresh();
this.render();
});
} else {
console.log('saving....', filePickerResult[0]);
onUpdate(field.id, filePickerResult[0]);
this.context.propertyPane.refresh();
this.render();
}
}
},
hideLocalUploadTab: false,
hideLocalMultipleUploadTab: true,
hideLinkUploadTab: false,
});
},
required: false,
},
],
disabled: false,
},
Related
I'm learning React.js and this is a table showing which user has which items.
I would like to have a button for each item and delete the corresponding item.
How do you have or {FaTrash} icon in a const object?
This is my full code below
const columns = [
{
name: "Username",
selector: "username",
sortable: true
},
{
name: "Email",
selector: "email",
sortable: true
},
{
name: "Item",
selector: "items",
sortable: true,
right: true
},
{
name: "Action",
value: <button>Edit</button>
}
]
const Admin = () => {
const [data, setData] = useState(allUsers);
const handleRowClicked = row => {
const updatedData = data.map(item => {
if (row.id !== item.id) {
return item;
}
return {
...item,
toggleSelected: !item.toggleSelected
};
});
setData(updatedData);
}
return ( <>
<div className='users p-5'>
<DataTable
title="Users"
columns={columns}
data={data}
defaultSortField="title"
pagination
onRowClicked={handleRowClicked}
/>
</div>
</> );
}
export default Admin;
I used to pass a function that returns a piece of layout with handler
{
name: "Action",
actionRenderer: ({ index, item }) => {
return (
<button onClick={() => onhandle(item)}>
ActionName <!--or icon component-->
</button>
)
}
},
Than you need to create <DataTableRow> component wich will render each object in your columns array. Somewhere in the <DataTableRow> you will be able to access to actionRenderer and your data item:
<div>{actionColumn.actionRenderer({ index, item })}</div>
Someone could help me create array for images inside attributes?
At the moment I did:
attributes: {
Bg_URL: { type: 'string' },
Bg_ID: { type: 'number' },
Bg_ALT: { type: 'string'},
icon_1_URL: { type: 'string' },
icon_1_ID: { type: 'number' },
icon_1_ALT: { type: 'string'},
icon_2_URL: { type: 'string' },
icon_2_ID: { type: 'number' },
icon_2_ALT: { type: 'string'},
icon_3_URL: { type: 'string' },
icon_3_ID: { type: 'number' },
icon_3_ALT: { type: 'string'},
},
Then Update function:
const onSelectImage = (name) => (e) => {
setAttributes({
[name+'_URL']: e.url,
[name+'_ID']: e.id,
[name+'_ALT']: e.alt,
});
};
And MediaUpload:
<MediaUpload
onSelect={onSelectImage('Bg')}
type="image"
value={attributes.Bg_ID}
render={({open}) => (
<figure className={!attributes.Bg_ID ? ' image-button' : ' image-preview'} onClick={open} >
{!attributes.Bg_ID ? __("Upload Image") : <img src={attributes.Bg_URL} />}
</figure>
)}
/>
So ok, I did one function to update the specific image in attributes, but I would like to have images array there, which would store all block images, and from onSelect I could pass name/id/key of that image to store, which I would call
images.key.url
Ideally I would see the same for RichText, where I don't need to define each of it separate, but would have a array "content", and there something like this?
value={ attributes.content.heading }
onChange={ ( content.heading ) => setAttributes( { content.heading } ) }
where .heading don't need to be defined earlier, it is when adding the content.
Have anyone had the same idea and could help to archive that?
Thanks.
I would suggest storing the images in an array attribute. Then you can use the MediaUpload component to create and edit a "gallery".
The onSelectMeida function:
const onSelectMedia = (media) => {
setAttributes({
images: [{
id: media.id,
url: media.url,
alt: media.alt,
}],
});
};
The MediaUpload component:
<MediaUploadCheck>
<MediaUpload
multiple={ true }
gallery={ true }
onSelect={ (media) => onSelectMedia(media) }
allowedTypes={ ['image'] }
accept="image/*"
value={ images.map(item => item.id) }
render={ ({open}) => {
return (
<Fragment>
<Button
isPrimary={ true }
onClick={ (event) => {
event.stopPropagation();
open();
} }
>
{ images.length > 0 ? __('Edit Images', 'pb') : __('Select Images', 'pb') }
</Button>
</Fragment>
);
} }
/>
</MediaUploadCheck>
Then you can render the images using a map:
{ images.length > 0 && images.map(image => {
<img
key={ image.id }
src={ image.url }
alt={ image.alt }
/>
}) }
I am trying to implement toggle functionality, by using this functionality user can select desired single preference, and also the user can select all preferences by using the "Select All" button. I have implemented the code that is supporting a single selection I want to make select all functionality.
This is how i am handling toggle
const toggleItem = useCallback((isToggled, value) => {
if (isToggled) {
setToggledItems((prevState) => [...prevState, value]);
} else {
setToggledItems((prevState) => [...prevState.filter((item) => item !== value)]);
}
}, []);
const [toggledItems, setToggledItems] = useState([]);
var eventsnfo = [
{
icon: '',
title: 'select All',
subTitle: '',
},
{
icon: 'event1',
title: 'event1',
subTitle: 'event1desc',
},
{
icon: 'event2',
title: 'event2',
subTitle: 'event2desc',
},
{
icon: 'event3',
title: 'event3',
subTitle: 'event3desc',
},
{
icon: 'event4',
title: 'event4',
subTitle: 'event4desc',
},
];
this is how i am loading all toggle sections
<div>
{eventsnfo?.map(({ icon, title, subTitle }, index) => {
return (
<>
<div key={index} className='events__firstSection'>
<div className='events__agreeToAllContainer'>
{icon && (
<Icon name={icon} className='events__noticeIcon' isForceDarkMode />
)}
<div className={icon ? 'events__text' : 'events__text events__leftAlign '}>
{title}
</div>
</div>
<Toggle
containerClass='events__toggle'
checked={toggledItems.includes(title)}
onToggle={(isToggled) => toggleItem(isToggled, title)}
/>
</div>
{subTitle && <div className='events__description'>{subTitle}</div>}
<div className={index !== eventsnfo.length - 1 && 'events__divider'}></div>
</>
);
})}
</div>;
I think you can toggle all by changing your toggleItem function
const toggleItem = (isToggled, value) => {
let items = [...toggledItems];
if (isToggled) {
items =
value === "select All"
? eventsnfo?.map((events) => events.title)
: [...items, value];
if (items?.length === eventsnfo?.length - 1) {
items.push("select All");
}
} else {
items =
value === "select All"
? []
: [...items.filter((item) => item !== value && item !== "select All")];
}
setToggledItems(items);
};
Working Demo
I've been struggling with what seems like a simple solution for far too long. I'm new to typescript and new to react.
I'm trying to use the react-mulit-carousel NPM package.
I'm able to get the customButtonGroup to work successfully in the sandbox:
https://codesandbox.io/s/fervent-rain-332mn?file=/src/App.js:834-913
But when I try to implement that in my SPFX solution i get the following error:
Type '{}' is missing the following properties from type '{ [x: string]: any; next: any; previous: any; goToSlide: any; }': next, previous, goToSlide
import * as React from 'react';
import { IBrandsCarouselProps } from './IBrandsCarouselProps';
import { IBrandsCarouselState } from './IBrandsCarouselState';
import { IBrand } from './IBrand';
import styles from '../../../styles/styles.module.scss';
import { SPHttpClient } from '#microsoft/sp-http';
import Carousel from 'react-multi-carousel';
import 'react-multi-carousel/lib/styles.css';
import '../../../styles/react-carousel.scss';
import { getNextElement } from 'office-ui-fabric-react';
const responsive = {
desktop: {
breakpoint: { max: 4000, min: 768 },
items: 4,
partialVisibilityGutter: 0
},
tablet: {
breakpoint: { max: 768, min: 480 },
items: 3,
partialVisibilityGutter: 30
},
mobile: {
breakpoint: { max: 480, min: 0 },
items: 2,
partialVisibilityGutter: 30
}
};
export default class BrandsCarousel extends React.Component<IBrandsCarouselProps, IBrandsCarouselState>{
constructor(props) {
super(props);
this.state = {
displayedBrands: [],
isLoading: true
};
}
/**
* Loads data from a list by using a cached view
*/
public loadBrandsFromList(): Promise<IBrand[]> {
const queryUrlGetAllItems: string = `[[HIDDEN]]`;
return this.props.context.spHttpClient.get(
queryUrlGetAllItems,
SPHttpClient.configurations.v1)
.then(
(response: any) => {
if (response.status >= 200 && response.status < 300) {
return response.json();
} else {
return Promise.resolve(new Error(JSON.stringify(response)));
}
})
.then((data: any) => {
let documents: IBrand[] = [];
if (data) {
for (let i = 0; i < data.value.length; i++) {
let item = data.value[i];
var doc: IBrand = {
Title: item.Title,
Image: item.Image.Url ? item.Image.Url : "No Image Set",
Url: item.Url.Url,
Business: item.Business
};
documents.push(doc);
}
}
return documents;
}).catch((ex) => {
// console.log("readDocumentsFromLibrary > spHttpClient.get()...catch:", ex);
throw ex;
});
}
public render(): React.ReactElement<IBrandsCarouselProps> {
// Sorting is Done in the Rest Call
let items = this.state.displayedBrands;
// create a new list that filters by the tags
// Business is an array of strings
// If the item has an array value that matches the Props Business
if (this.props.Business != "All") {
let filteredItems = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.Business.indexOf(this.props.Business) > -1) {
filteredItems.push(item);
}
}
items = filteredItems;
}
const ButtonGroup = ({ next, previous, goToSlide, ...rest }) => {
const {
carouselState: { currentSlide }
} = rest;
return (
<div className="carousel-button-group">
<div
className={currentSlide === 0 ? "disable" : ""}
onClick={() => previous()}
>
Prev
</div>
<div onClick={() => next()}>Next</div>
<div onClick={() => goToSlide(currentSlide + 1)}> Go to any slide </div>
</div>
);
};
return (
<div className={styles["brands-slider"] + " " + styles["card-docs-slider"] + " hub-carousel"}>
{this.props.IsTitle && this.props.Title != "" &&
<div className={styles["widget-header"]}>
<span className={styles["view"]}>{this.props.Title}</span>
</div>
}
<div className={styles["card-slider"]}>
{items && items.length > 0 &&
<Carousel
responsive={responsive}
arrows
additionalTransfrom={0}
itemClass={"react-carousel-item"}
minimumTouchDrag={80}
partialVisible
renderButtonGroupOutside
customButtonGroup={<ButtonGroup />}
>
{items.map((item) => {
return (
<a href={item.Url} className={styles["block-link"]} target="_blank">
<img src={item.Image} alt={item.Title} />
</a>
);
})
}
</Carousel>
}
{items && items.length == 0 &&
<p>No Brands found. Please, check the List</p>
}
</div>
</div>
);
}
public componentDidMount() {
this.loadBrandsFromList().then(
//resolve
(documents: IBrand[]) => {
this.setState({
displayedBrands: documents,
isLoading: false,
});
},
//reject
(data: any) => {
this.setState({
displayedBrands: [],
isLoading: false,
isErrorOccured: true,
errorMessage: data
});
}
).catch((ex) => {
this.setState({
displayedBrands: [],
isLoading: false,
isErrorOccured: true,
errorMessage: ex.errorMessage
});
});
}
}
Any help would be greatly appreciated. Thank you!
I was able to figure it out. I needed to pass parameters. Oops!
Hopefully this can help out another JSX, Typescript, React beginner in the future.
<Carousel
responsive={responsive}
arrows
additionalTransfrom={0}
itemClass={"react-carousel-item"}
minimumTouchDrag={80}
partialVisible
renderButtonGroupOutside
customButtonGroup={<ButtonGroup
next={this.props.next}
previous={this.props.previous}
rest={this.props.rest}
/>}
>
Here's the Custom Button group if it helps as well. I couldn't find the documentation to hide the next button.
const ButtonGroup = ({ next, previous, ...rest }) => {
const {
carouselState: { currentSlide, totalItems, slidesToShow }
} = rest;
return (
<div className="carousel-button-group">
<button aria-label="Go to previous slide"
className={currentSlide === 0 ? "disable" : "react-multiple-carousel__arrow react-multiple-carousel__arrow--left"}
onClick={() => previous()}></button>
<button aria-label="Go to next slide"
className={currentSlide === totalItems - slidesToShow ? "disable" : "react-multiple-carousel__arrow react-multiple-carousel__arrow--right"}
onClick={() => next()}></button>
</div>
);
};
I have a reactjs component which displays an antd table of which one of the columns is to execute an action to archive the item in the row. If someone clicks on Archive I want it to show a popconfirm with yes/no confirmation before it moves forward and archives the item.
Everything works fine until I add the Popconfirm block. Then I get the below error. I think that there is something wrong with my usage of onconfirm and oncancel in the popconfirm but i'm just not getting something probably obvious here. Appreciate any feedback!
import React, { Component } from 'react';
import { connect } from 'react-redux';
import selectProperties from '../selectors/properties';
import { Table, Tag, Divider, Popconfirm, message } from 'antd';
export class PropertyList extends React.Component {
constructor(){
super();
this.columns = [
{
title: 'Address',
dataIndex: 'street',
key: 'street',
render: text => <a>{text}</a>,
},
{
title: 'City',
dataIndex: 'city',
key: 'city',
},
{
title: 'State',
dataIndex: 'state',
key: 'state',
},
{
title: 'Workflow',
key: 'workflow',
dataIndex: 'workflow',
sorter: (a, b) => a.workflow.length - b.workflow.length,
sortDirections: ['descend'],
render: workflow => {
let color = 'geekblue';
if (workflow === 'Inspection' || workflow === 'Maintenance' || workflow === 'Cleaning') {
color = 'volcano';
}
else if (workflow === 'Rented') {
color = 'green';
}
return (
<span>
<Tag color={color} key={workflow}>
{workflow.toUpperCase()}
</Tag>
</span>
);
},
},
{
title: 'Action',
key: 'action',
render: (text, record) => (
<span>
<a>Edit</a>
<Divider type="vertical" />
<Popconfirm
title="Are you sure?"
onConfirm={this.confirm(record)}
onCancel={this.cancel}
okText="Yes"
cancelText="No"
>
Archive
</Popconfirm>
</span>
),
},
];
}
confirm = (record) => {
message.success('Archived');
console.log("confirm function.. record");
console.log(record);
}
cancel = () => {
message.error('Cancelled');
}
render() {
console.log("PropertyList render");
console.log(this.props);
console.log(this.props.properties);
console.log(this.columns);
return (
<div className="content-container">
<div className="list-body">
{
this.props.properties.length === 0 ? (
<div className="list-item list-item--message">
<span>No properties. Add some!</span>
</div>
) : (
<Table
rowKey="id"
dataSource={this.props.properties}
columns={this.columns}
pagination = { false }
footer={() => ''}
/>
)
}
</div>
</div>
)
}
};
const mapStateToProps = (state) => {
console.log("PropertyList mapStateToProps..");
console.log(state);
return {
properties: selectProperties(state.properties, state.filters)
};
};
const mapDispatchToProps = (dispatch) => ({
updateProperty: (id, property) => dispatch(editProperty(id, property))
});
export default connect(mapStateToProps, mapDispatchToProps)(PropertyList);
you are invoking the method confirm (in onConfirm) immediately when it renders the Table rows.
Change:
onConfirm={this.confirm(record)}
To:
onConfirm={() => this.confirm(record)}