Support for localization

This commit is contained in:
Nanddeep Nachan 2020-07-10 14:19:07 +00:00
parent daa67c1fa7
commit 7727eafd28
13 changed files with 849 additions and 307 deletions

View File

@ -64,6 +64,8 @@ The sample also provisions sample data to the "Timeline" list, which can be used
Below NPM package is used to develop this sample.
1. @pnp/sp (https://www.npmjs.com/package/@pnp/sp)
2. @pnp/spfx-controls-react (https://pnp.github.io/sp-dev-fx-controls-react/)
3. @pnp/spfx-property-controls (https://pnp.github.io/sp-dev-fx-property-controls/)
## Used SharePoint Framework Version
@ -121,3 +123,5 @@ This sample web part displays list of events in chronological order with data st
[figure5]: ./assets/layout-horizontal.png
[figure6]: ./assets/list-schema.png
[figure7]: ./assets/list-sample-data.png
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-timeline" />

View File

@ -13,6 +13,8 @@
},
"externals": {},
"localizedResources": {
"TimelineWebPartStrings": "lib/webparts/timeline/loc/{locale}.js"
"TimelineWebPartStrings": "lib/webparts/timeline/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,8 @@
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@pnp/sp": "^2.0.6",
"@pnp/spfx-controls-react": "1.19.0",
"@pnp/spfx-property-controls": "1.19.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",

View File

@ -17,14 +17,14 @@ export default class TimelineService {
/**
* Get timeline activity by id.
* @param listTitle
* @param listId
* @param id
*/
public async getTimelineActivity(listTitle: string, id: number): Promise<ITimelineActivity> {
public async getTimelineActivity(listId: string, id: number): Promise<ITimelineActivity> {
let returnTimelineActivity: ITimelineActivity = undefined;
try {
let activity: any = await sp.web.lists.getByTitle(listTitle).items.usingCaching().getById(id)
let activity: any = await sp.web.lists.getById(listId).items.usingCaching().getById(id)
.select("Id", "Title", "SPFxTimelineLink", "SPFxTimelineDate", "SPFxTimelinePicture", "SPFxTimelineDescription")
.get();
@ -46,15 +46,15 @@ export default class TimelineService {
/**
* Get all timeline activities
* @param listTitle
* @param listId
* @param sortOrder
*/
public async getTimelineActivities(listTitle: string, sortOrder: string): Promise<ITimelineActivity[]> {
public async getTimelineActivities(listId: string, sortOrder: string): Promise<ITimelineActivity[]> {
let returnTimelineActivities: ITimelineActivity[] = [];
let sortOrderAsc: boolean = (sortOrder === "asc");
try {
let activities: any[] = await sp.web.lists.getByTitle(listTitle).items
let activities: any[] = await sp.web.lists.getById(listId).items
.select("Id", "Title", "SPFxTimelineLink", "SPFxTimelineDate", "SPFxTimelinePicture", "SPFxTimelineDescription")
.orderBy("SPFxTimelineDate", sortOrderAsc)
.get();
@ -81,10 +81,10 @@ export default class TimelineService {
/**
* Adds timeline activity to SP list.
* @param listTitle
* @param listId
* @param newTimelineActivity
*/
public async addTimelineActivity(listTitle: string, newTimelineActivity: ITimelineActivity) {
public async addTimelineActivity(listId: string, newTimelineActivity: ITimelineActivity) {
try {
let addData: ITypedHash<any> = {
Title: newTimelineActivity.activityTitle,
@ -110,7 +110,7 @@ export default class TimelineService {
};
}
await sp.web.lists.getByTitle(listTitle).items.add(addData);
await sp.web.lists.getById(listId).items.add(addData);
}
catch (error) {
console.log(error);
@ -120,10 +120,10 @@ export default class TimelineService {
/**
* Updates timeline activity to SP list by id.
* @param listTitle
* @param listId
* @param updateTimelineActivity
*/
public async updateTimelineActivity(listTitle: string, updateTimelineActivity: ITimelineActivity) {
public async updateTimelineActivity(listId: string, updateTimelineActivity: ITimelineActivity) {
try {
let updateItem: ITypedHash<any> = {
Title: updateTimelineActivity.activityTitle,
@ -153,7 +153,7 @@ export default class TimelineService {
};
}
await sp.web.lists.getByTitle(listTitle).items.getById(updateTimelineActivity.id).update(updateItem).then((value: any) => {
await sp.web.lists.getById(listId).items.getById(updateTimelineActivity.id).update(updateItem).then((value: any) => {
console.log(value);
});
}
@ -165,12 +165,12 @@ export default class TimelineService {
/**
* Deletes timeline activity from SP list.
* @param listTitle
* @param listId
* @param deleteTimelineActivity
*/
public async deleteTimelineActivity(listTitle: string, deleteTimelineActivity: ITimelineActivity) {
public async deleteTimelineActivity(listId: string, deleteTimelineActivity: ITimelineActivity) {
try {
await sp.web.lists.getByTitle(listTitle).items.getById(deleteTimelineActivity.id).delete();
await sp.web.lists.getById(listId).items.getById(deleteTimelineActivity.id).delete();
}
catch (error) {
return Promise.reject(error);

View File

@ -22,11 +22,9 @@
"officeFabricIconFontName": "TimelineProgress",
"properties": {
"description": "Timeline Events",
"listName": "Timeline",
"layout": "Vertical",
"showImage": true,
"showDescription": true,
"dateFormat": "MM/DD/yyyy",
"sortOrder": "asc"
}
}]

View File

@ -11,7 +11,7 @@ import * as strings from 'TimelineWebPartStrings';
import Timeline from './components/Timeline';
import { ITimelineProps } from './components/ITimelineProps';
import TimelineService from '../../services/TimelineService';
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy, ISPList } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker';
export interface ITimelineWebPartProps {
description: string;
@ -19,11 +19,11 @@ export interface ITimelineWebPartProps {
layout: string;
showImage: boolean;
showDescription: boolean;
dateFormat : string;
dateFormat: string;
sortOrder: string;
}
export default class TimelineWebPart extends BaseClientSideWebPart <ITimelineWebPartProps> {
export default class TimelineWebPart extends BaseClientSideWebPart<ITimelineWebPartProps> {
private TimelineService: TimelineService = null;
protected onInit(): Promise<void> {
@ -71,30 +71,44 @@ export default class TimelineWebPart extends BaseClientSideWebPart <ITimelineWeb
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
}),
PropertyPaneTextField('listName', {
label: strings.ListNameFieldLabel
PropertyFieldListPicker('listName', {
label: strings.ListNameFieldLabel,
selectedList: this.properties.listName,
includeHidden: false,
orderBy: PropertyFieldListPickerOrderBy.Title,
disabled: false,
onPropertyChange: this.onPropertyPaneFieldChanged.bind(this),
properties: this.properties,
context: this.context,
onGetErrorMessage: null,
deferredValidationTime: 0,
key: 'listPickerFieldId',
baseTemplate: 100
}),
PropertyPaneDropdown('layout', {
label: strings.LayoutFieldLabel,
options: [
{ key: 'Vertical', text: 'Vertical' },
{ key: 'Horizontal', text: 'Horizontal' }
{ key: 'Vertical', text: strings.VerticalLabel },
{ key: 'Horizontal', text: strings.HorizontalLabel }
]
}),
PropertyPaneToggle('showImage', {
label: strings.ShowImageFieldLabel,checked:true
label: strings.ShowImageFieldLabel,
checked: true
}),
PropertyPaneToggle('showDescription', {
label: strings.ShowDescriptionFieldLabel, checked: true
label: strings.ShowDescriptionFieldLabel,
checked: true
}),
PropertyPaneTextField('dateFormat', {
label: strings.DateFormatFieldLabel
label: strings.DateFormatFieldLabel,
value: strings.DateFormatText
}),
PropertyPaneDropdown('sortOrder', {
label: strings.SortOrderFieldLabel,
options: [
{ key: 'asc', text: 'Ascending' },
{ key: 'desc', text: 'Descending' }
{ key: 'asc', text: strings.AscendingLabel },
{ key: 'desc', text: strings.DescendingLabel }
]
})
]

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import styles from './timelineEvent.module.scss';
import * as strings from 'TimelineWebPartStrings';
import { IEventProps } from './ITimeLineEventProps';
import { IEventState } from './ITimeLineEventState';
import { ITimelineActivity } from '../../../../models/ITimelineActivity';
@ -219,13 +220,13 @@ export class TimelineEvent extends React.Component<IEventProps, IEventState> {
<div>
<Dialog
isOpen={this.props.showPanel}
closeButtonAriaLabel="Close"
closeButtonAriaLabel={strings.CloseLabel}
dialogContentProps={{
type: DialogType.normal,
title:
this.props.panelMode == 2
? "Edit Timeline Event"
: "Create Timeline Event",
? strings.EditEventLabel
: strings.AddEventLabel,
showCloseButton: true,
}}
onDismiss={this.hidePanel}
@ -235,7 +236,7 @@ export class TimelineEvent extends React.Component<IEventProps, IEventState> {
>
<div>
<TextField
label="Title"
label={strings.TitleLabel}
required
value={
this.state.eventData
@ -247,7 +248,7 @@ export class TimelineEvent extends React.Component<IEventProps, IEventState> {
/>
</div>
<Label>
Date
{strings.DateLabel}
</Label>
<div
@ -339,14 +340,14 @@ export class TimelineEvent extends React.Component<IEventProps, IEventState> {
<div>
<TextField
label="Description"
label={strings.DescriptionLabel}
value={this.state.activityDescription} onChange={this.onDescriptionChange}
multiline
/>
</div>
<div>
<TextField
label="Picture URL"
label={strings.PictureURLLabel}
value={
this.state.eventData
? this.state.eventData.activityPictureUrl ? this.state.eventData.activityPictureUrl["Url"] : ''
@ -358,7 +359,7 @@ export class TimelineEvent extends React.Component<IEventProps, IEventState> {
</div>
<div>
<TextField
label="Link URL"
label={strings.LinkURLLabel}
value={
this.state.eventData
? this.state.eventData.activityLink ? this.state.eventData.activityLink["Url"] : ''
@ -369,7 +370,7 @@ export class TimelineEvent extends React.Component<IEventProps, IEventState> {
/>
</div>
<DialogFooter>
<PrimaryButton onClick={this.onSave} text={this.props.panelMode == 2 ? "Update Event" : "Create Event"} />
<PrimaryButton onClick={this.onSave} text={this.props.panelMode == 2 ? strings.UpdateEventLabel : strings.AddEventLabel} />
<DefaultButton onClick={this.hidePanel} text="Cancel" />
</DialogFooter>
</Dialog>

View File

@ -1,27 +1,15 @@
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
:global(.ms-Dialog-inner) {
padding-left: 50px !important;
padding-right: 50px !important;
}
:global(.ms-Dialog-title) {
.dialogOverride {
:global(.ms-Dialog-title) {
color: white !important;
background-color: $ms-color-themePrimary !important;
font-size: 16px;
font-weight: bold;
padding: 18px !important;
}
}
:global(i.ms-Button-icon[data-icon-name="Cancel"]) {
width: 25px;
height: 25px;
background-repeat: no-repeat;
background-size: 23px 23px;
:global(i.ms-Button-icon[data-icon-name="Cancel"]) {
color: white !important;
}
}
.dialogOverride {
@media (max-width: $ms-screen-max-md) {
// need to remove justify center when on a small screen so dialog can use full width
:global(.ms-Dialog) {

View File

@ -1,10 +1,11 @@
import * as React from 'react';
import styles from './Timeline.module.scss';
import * as strings from 'TimelineWebPartStrings';
import { ITimelineProps } from './ITimelineProps';
import { ITimelineState } from './ITimelineState';
import { escape } from '@microsoft/sp-lodash-subset';
import TimelineService from '../../../services/TimelineService';
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import TimelineActivity from "./TimelineActivity";
import { ITimelineActivity } from "../../../models/ITimelineActivity";
import { SPPermission } from '@microsoft/sp-page-context';
@ -36,9 +37,25 @@ export default class Timeline extends React.Component<ITimelineProps, ITimelineS
}
}
private _onConfigure = () => {
// Context of the web part
this.props.context.propertyPane.open();
}
public render(): React.ReactElement<ITimelineProps> {
return (
<div className={styles.timeline}>
{
this.state.timelineActivities.length == 0 &&
<Placeholder iconName='Edit'
iconText={strings.ConfigureWebPartLabel}
description={strings.ConfigureDescription}
buttonLabel={strings.ConfigureLabel}
onConfigure={this._onConfigure} />
}
{this.state.timelineActivities.length > 0 &&
<>
<h1>{this.props.description}</h1>
<div className={this.props.layout == "Vertical" ? `${styles.timelineContainerVertical}` : `${styles.timelineContainerHorizontal}`}>
{
@ -59,6 +76,8 @@ export default class Timeline extends React.Component<ITimelineProps, ITimelineS
);
})}
</div>
</>
}
</div>
);
}
@ -66,13 +85,17 @@ export default class Timeline extends React.Component<ITimelineProps, ITimelineS
public componentDidMount(): void {
this.TimelineService.getTimelineActivities(this.props.listName, this.props.sortOrder).then((activities: ITimelineActivity[]) => {
this.setState({ timelineActivities: activities });
}).catch((error: any) => {
this.setState({ timelineActivities: [] });
});
}
public componentWillReceiveProps(nextProps: ITimelineProps) {
if (this.props.sortOrder !== nextProps.sortOrder) {
this.TimelineService.getTimelineActivities(this.props.listName, nextProps.sortOrder).then((activities: ITimelineActivity[]) => {
if (this.props.sortOrder !== nextProps.sortOrder || this.props.listName !== nextProps.listName) {
this.TimelineService.getTimelineActivities(nextProps.listName, nextProps.sortOrder).then((activities: ITimelineActivity[]) => {
this.setState({ timelineActivities: activities });
}).catch((error: any) => {
this.setState({ timelineActivities: [] });
});
}
}

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import styles from './Timeline.module.scss';
import * as strings from 'TimelineWebPartStrings';
import { escape } from '@microsoft/sp-lodash-subset';
import { ITimelineActivity } from "../../../models";
import { Card, ICardTokens, ICardSectionStyles, ICardSectionTokens } from '@uifabric/react-cards';
@ -178,14 +179,14 @@ export default class TimelineActivity extends React.Component<IActivityProps, IA
{
this.props.canEdit &&
<div className={this.state.layout == "Vertical" ? `${styles.timelineAddVertical}` : `${styles.timelineAddHorizontal}`}>
<IconButton iconProps={addToIcon} title="Add Timeline Event" ariaLabel="Add Activity" className={styles.addToButton} onClick={this.createEvent} />
<IconButton iconProps={addToIcon} title={strings.AddEventLabel} ariaLabel={strings.AddEventLabel} className={styles.addToButton} onClick={this.createEvent} />
</div>
}
<Dialog type={DialogType.normal}
hidden={!this.state.showDeleteDialog}
title='Delete event?'
subText='Do you want to delete this event?'
title={strings.DeleteEventLabel}
subText={strings.DeleteEventConfirmationLabel}
isBlocking={true}
containerClassName={'ms-dialogMainOverride'}>
<DialogFooter>
@ -220,7 +221,6 @@ export default class TimelineActivity extends React.Component<IActivityProps, IA
)}
<div className={styles.timelineCard}>
<Card
aria-label="Clickable horizontal card "
horizontal
tokens={cardTokens}
>
@ -229,7 +229,7 @@ export default class TimelineActivity extends React.Component<IActivityProps, IA
<Card.Item fill>
<Image
src={activity.activityPictureUrl ? activity.activityPictureUrl["Url"] : ''}
alt="Placeholder image."
alt={activity.activityTitle}
width="100px"
height="100px"
/>
@ -259,7 +259,7 @@ export default class TimelineActivity extends React.Component<IActivityProps, IA
>
{canEdit &&
<IconButton
id="ContextualMenuButton1"
id="ContextualMenuButtonMore"
text=""
split={false}
iconProps={{ iconName: "MoreVertical" }}
@ -270,7 +270,7 @@ export default class TimelineActivity extends React.Component<IActivityProps, IA
items: [
{
key: "Edit",
name: "Edit",
name: strings.EditEventLabel,
onClick: (event) => {
this.setState({ selectedEvent: activity });
this.editEvent();
@ -282,7 +282,7 @@ export default class TimelineActivity extends React.Component<IActivityProps, IA
},
{
key: "Delete",
name: "Delete",
name: strings.DeleteEventLabel,
onClick: (event) => {
this.setState({
selectedEvent: activity,

View File

@ -1,13 +1,32 @@
define([], function() {
define([], function () {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "WebPart Title",
"ListNameFieldLabel":"List Name",
"LayoutFieldLabel":"Layout Type",
"ShowImageFieldLabel":"Show Image",
"ListNameFieldLabel": "List Name",
"LayoutFieldLabel": "Layout Type",
"ShowImageFieldLabel": "Show Image",
"ShowDescriptionFieldLabel": "Show Description",
"DateFormatFieldLabel": "Date Format",
"SortOrderFieldLabel": "Sort Direction"
"SortOrderFieldLabel": "Sort Direction",
"EditEventLabel": "Edit Event",
"UpdateEventLabel": "Update Event",
"AddEventLabel": "Add Event",
"DeleteEventLabel": "Delete Event",
"DeleteEventConfirmationLabel": "Do you want to delete this event?",
"CloseLabel": "Close",
"TitleLabel": "Title",
"DateLabel": "Date",
"DescriptionLabel": "Description",
"PictureURLLabel": "Picture URL",
"LinkURLLabel": "Link URL",
"ConfigureWebPartLabel": "Configure your timeline",
"ConfigureDescription": "Please select the list with timeline information",
"ConfigureLabel": "Configure",
"AscendingLabel": "Ascending",
"DescendingLabel": "Descending",
"VerticalLabel": "Vertical",
"HorizontalLabel": "Horizontal",
"DateFormatText": "MM/DD/yyyy"
}
});

View File

@ -8,6 +8,25 @@ declare interface ITimelineWebPartStrings {
ShowDescriptionFieldLabel: string;
DateFormatFieldLabel: string;
SortOrderFieldLabel: string;
EditEventLabel: string;
UpdateEventLabel: string;
AddEventLabel: string;
DeleteEventLabel: string;
DeleteEventConfirmationLabel: string;
CloseLabel: string;
TitleLabel: string;
DateLabel: string;
DescriptionLabel: string;
PictureURLLabel: string;
LinkURLLabel: string;
ConfigureWebPartLabel: string;
ConfigureDescription: string;
ConfigureLabel: string;
AscendingLabel: string;
DescendingLabel: string;
VerticalLabel: string;
HorizontalLabel: string;
DateFormatText: string;
}
declare module 'TimelineWebPartStrings' {