new sample Flight-tracker

This commit is contained in:
João Mendes 2022-11-13 21:35:35 +00:00
parent 6fe61af2ca
commit 10342b46b2
16 changed files with 190 additions and 267 deletions

View File

@ -5,10 +5,10 @@
This WebPart allows to track all flights from a selected airport, date and information type.
The SPFx use external API (https://aerodatabox.p.rapidapi.com/flights/number/) to get data of flight, please see https://rapidapi.com/aedbx-aedbx/api/aerodatabox/ to get more information. There is some restritions on this API, the number of requests is limited (free version)
![SharePoint View](assets/FQzNLB4XwAIFMRh.jpg "SharePoint View")
![Teams View](assets/FQzO9YjWUAgFlrU.jpg "Teams View")
![SharePoint View](./src/assets/sharepoint.png "SharePoint View")
![Teams View](./src/assets/teams.jpg "Teams View")
## Compatibility
React-
![SPFx 1.15](https://img.shields.io/badge/SPFx-1.15-green.svg)
![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg)
@ -39,25 +39,36 @@ The SPFx use external API (https://aerodatabox.p.rapidapi.com/flights/number/) t
Solution|Author(s)
--------|---------
react-fluentui-9 | [Nick Brown](https://github.com/techienickb) ([@techienickb](https://twitter.com/techienickb) / [Jisc](https://jisc.ac.uk))
react-flighttracker | [João Mendes](https://github.com/joaojmendes) ([@joaojmendes](https://twitter.com/joaojmendes)), StaffBase |
## Version history
Version|Date|Comments
-------|----|--------
1.0|April 20, 2022|Initial release
1.0|November 11, 2022|Initial release
- Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-fluentui-9) then unzip it)
- From your command-line, change your current directory to the directory containing this sample (`react-fluentui-9`, located under `samples`)
- in the command-line run:
- `npm install`
- `gulp serve`
## Minimal Path to Awesome
> This sample can also be opened with [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview). Visit <https://aka.ms/spfx-devcontainer> for further instructions.
- Clone this repository
- Ensure that you are at the solution folder
- in the command line run:
- `npm install`
- `gulp build --ship`
- `gulp bundle --ship`
- `gulp package-solution --ship`
- Browse to your SharePoint app catalog and load the SPFx package.
- Browse to your SharePoint Admin Center and under advanced you will need to open Api Access and allow the requests for Microsoft Graph.
- If you have the APIs permissions already allowed you can follow the below steps.
- in the command line run:
*`npm install`
* `gulp serve --nobrowser`
- browse to your hosted workbench [https://YOURTENANT.sharepoint.com/sites/_layouts/15/workbench.aspx](https://YOURTENANT.sharepoint.com/sites/_layouts/15/workbench.aspx) and add the adaptive card extension.
## Features
Very simple demo, the handling of the theme provider is interesting implementing it and handling the custom themes SharePoint can use.
External API , Global State Management with Recoil Library, React Hooks , CCS-In-JS Styling
## References
@ -88,4 +99,4 @@ Finally, if you have an idea for improvement, [make a suggestion](https://github
**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.**
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-fluentui-9" />
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-flighttracler" />

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 453 KiB

View File

@ -25,6 +25,7 @@ export const FlightTrackerControl: React.FunctionComponent<IFlightTrackerProps>
context: context,
numberItemsPerPage: numberItemsPerPage,
webpartContainerWidth: webpartContainerWidth,
});
}, [isDarkTheme, hasTeamsContext, currentTheme, context, setGlobalState, webpartContainerWidth]);

View File

@ -11,5 +11,6 @@ export interface IFlightTrackerProps {
numberItemsPerPage: number;
displayMode: DisplayMode;
updateProperty: (value: string) => void;
webpartContainerWidth: number;
webpartContainerWidth: number;
}

View File

@ -38,7 +38,14 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
const [appState, setGlobalState] = useRecoilState(globalState);
const [airlineList, setAirlineList] = useRecoilState(airlineState);
const { mapFlightSchedules } = useMappingFlightSchedules();
const { selectedAirPort, selectedInformationType, selectedDate, numberItemsPerPage, selectedTime } = appState;
const {
selectedAirPort,
selectedInformationType,
selectedDate,
numberItemsPerPage,
selectedTime,
} = appState;
const [isLoadingItems, setIsLoadingItems] = React.useState<boolean>(true);
const [errorMessage, setErrorMessage] = React.useState<string>("");
const [listItems, setListItems] = React.useState<IFlightTrackerListItem[]>([]);
@ -51,6 +58,7 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
const [hasMore, setHasMore] = React.useState<boolean>(true);
const pageIndex = React.useRef<number>(0);
const currentInformationType = React.useRef(selectedInformationType);
const [timerId, setTimerId] = React.useState<number>(undefined);
const checkTypeInformationToScroll = React.useCallback(() => {
if (selectedInformationType !== currentInformationType.current) {
@ -59,6 +67,20 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
}
}, [selectedInformationType]);
const onRefresh = React.useCallback(async () => {
pageIndex.current = 0;
const currentDateTime = new Date();
setGlobalState(
(prevState) =>
({
...prevState,
selectedDate: currentDateTime,
selectedTime: currentDateTime,
} as IGlobalState)
);
setIsRefreshing(true);
}, [appState]);
React.useEffect(() => {
if (!isEmpty(airlines)) {
setAirlineList(airlines);
@ -139,17 +161,6 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
return (isLoadingFlightSchedules || loadingAirlines || isLoadingItems) && !showMessage;
}, [isLoadingFlightSchedules, loadingAirlines, isLoadingItems, errorFlightSchedules]);
const onRefresh = React.useCallback(async () => {
pageIndex.current = 0;
const currentDateTime = new Date();
setGlobalState((prevState) => ({
...prevState,
selectedDate: currentDateTime,
selectedTime: currentDateTime,
} as IGlobalState));
setIsRefreshing(true);
}, [appState]);
if (!selectedAirPort || !selectedInformationType) {
return null;
}

View File

@ -1,125 +0,0 @@
/* eslint-disable react/self-closing-comp */
import * as React from 'react';
import { IColumn } from 'office-ui-fabric-react/lib/DetailsList';
import { IFlightStatus } from '../../models/IFlightStatus';
import { IFlightTrackerListItem } from '../../models/IFlightTrackerListItem';
import { FlightStatus } from '../FlightStatus/FlightStatus';
import { useFlightTrackerStyles } from './useFlightTrackerStyles';
export interface IFlightTrackerListColumns {
getListColumns: () => IColumn[];
}
export const useFlightTrackerListColumns = ():IFlightTrackerListColumns => {
const { controlStyles } = useFlightTrackerStyles();
const getListColumns = React.useCallback((): IColumn[] => {
return [
{
key: "column1",
name: " flightCompanyImage",
className: controlStyles.fileIconCell,
iconClassName: controlStyles.fileIconHeaderIcon,
ariaLabel: "company image",
isIconOnly: true,
fieldName: "image",
minWidth: 70,
maxWidth: 70,
onRender: (item: IFlightTrackerListItem) => {
return (
<img src={item.flightCompanyImage} style={{width: 28, height: 28}} />
);
}
},
{
key: "column2",
name: "Air line",
fieldName: "flightCompany",
minWidth: 210,
maxWidth: 210,
isRowHeader: true,
isResizable: true,
data: "string",
isPadded: true,
},
{
key: "column6",
name: "Flight",
fieldName: "flightNumber",
minWidth: 70,
maxWidth: 90,
isResizable: true,
isCollapsible: true,
data: "number",
onRender: (item: IFlightTrackerListItem) => {
return <span>{item.flightNumber}</span>;
},
},
{
key: "column3",
name: "time",
fieldName: "flightTime",
minWidth: 70,
maxWidth: 90,
isResizable: true,
data: "number",
onRender: (item: IFlightTrackerListItem) => {
return <span>{item.flightTime}</span>;
},
isPadded: true,
},
{
key: "column4",
name: "Terminal",
fieldName: "flightTerminal",
minWidth: 70,
maxWidth: 90,
isResizable: true,
isCollapsible: true,
data: "string",
onRender: (item: IFlightTrackerListItem) => {
return <span>{item.flightTerminal}</span>;
},
isPadded: true,
},
{
key: "column5",
name: "Origem",
fieldName: "flightOrigin",
minWidth: 150,
maxWidth: 150,
isResizable: true,
isCollapsible: true,
data: "number",
onRender: (item: IFlightTrackerListItem) => {
return <span>{item.flightOrigin}</span>;
},
},
{
key: "column6",
name: "Status",
fieldName: "flightTimeStatus",
minWidth: 70,
maxWidth: 90,
isResizable: true,
isCollapsible: true,
data: "number",
onRender: (item: IFlightTrackerListItem) => {
const flightInfo:IFlightStatus = {
date: item.flightRealTime, status: item.flightTimeStatusText,
flightId: item.flightNumber
};
return <FlightStatus flightInfo={flightInfo}/>;
},
},
];
}, []);
return {getListColumns }
};

View File

@ -15,7 +15,7 @@ import { globalState } from '../../recoil/atoms';
export const useFlightTrackerStyles = () => {
const [globalStateApp] = useRecoilState(globalState);
const { currentTheme } = globalStateApp;
const { currentTheme, numberItemsPerPage } = globalStateApp;
const listHeaderStyles: ITextStyles = React.useMemo(() => {
return { root: { fontWeight: FontWeights.semibold, color: currentTheme?.semanticColors?.bodyText } };
@ -56,11 +56,11 @@ export const useFlightTrackerStyles = () => {
const scollableContainerStyles: Partial<IScrollablePaneStyles> = React.useMemo(() => {
return {
root: { position: "relative", height: 87 * 6 ,
root: { position: "relative", height: (87 * numberItemsPerPage) - 50 ,
},
contentContainer: { "::-webkit-scrollbar-thumb": {
backgroundColor: currentTheme?.semanticColors.bodyFrameBackground , },
backgroundColor: currentTheme?.palette.themeLight, },
"::-webkit-scrollbar": {
height: 10,
width: 7,
@ -69,7 +69,7 @@ export const useFlightTrackerStyles = () => {
"scrollbar-color": currentTheme?.semanticColors.bodyFrameBackground,
"scrollbar-width": "thin", },
};
}, [currentTheme]);
}, [currentTheme, numberItemsPerPage]);
const stackContainerStyles: IStackStyles= React.useMemo(() => {
return {

View File

@ -16,6 +16,7 @@ import {
import { useRecoilState } from 'recoil';
import { useAirports } from '../../hooks/useAirports';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { IAirport } from '../../models/IAirport';
import { globalState } from '../../recoil/atoms';
import { Airport } from './Airport';
@ -32,7 +33,9 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
const { searchAirportsByText } = useAirports();
const [selectedAirport, setSelectedAirports] = React.useState<ITag[]>([]);
const { controlStyles, selecteAirportPickerStyles } = useSelectAirportStyles();
const { context } = appState;
const SELECTED_AIRPORT_SESSION_STORAGE_KEY = "___selectedAirport___";
const [getSelectedAirportFromSessionStorage, SetSelectedAirporttoSessionStorage] = useLocalStorage();
const inputProps: IInputProps = React.useMemo(() => {
return {
placeholder: "Select an airport",
@ -93,14 +96,11 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
);
const onRenderSuggestionsItem = React.useCallback(
(props:ITagExtended, itemProps: ISuggestionItemProps<ITagExtended>) => {
(props: ITagExtended, itemProps: ISuggestionItemProps<ITagExtended>) => {
const { airportData } = props;
return (
<div className={controlStyles.pickerItemStyles}>
<Airport
airport={airportData}
/>
<Airport airport={airportData} />
</div>
);
},
@ -118,30 +118,50 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
airport={airport}
onRemove={(airport) => {
setSelectedAirports([]);
setAppState({ ...appState, selectedAirPort: undefined });
setAppState({ ...appState, selectedAirPort: undefined });
}}
/>
</div>
);
},
[appState]
[appState,setSelectedAirports,setAppState]
);
const pickerCalloutPropsStyles = (props: ICalloutContentStyleProps) => {
return { root: { width: divRef?.current?.offsetWidth } };
};
const onPickerChange = React.useCallback((items: ITag[]) => {
setAppState({ ...appState, selectedAirPort: (items[0] as ITagExtended)?.airportData });
}, [appState]);
const onPickerChange = React.useCallback(
(items: ITag[]) => {
SetSelectedAirporttoSessionStorage(
`${SELECTED_AIRPORT_SESSION_STORAGE_KEY}${context.instanceId}`,
items[0] as ITagExtended
);
setAppState({ ...appState, selectedAirPort: (items[0] as ITagExtended)?.airportData });
},
[appState]
);
React.useEffect(() => {
if (context) {
const selectedAirportInSessionStorage = getSelectedAirportFromSessionStorage(
`${SELECTED_AIRPORT_SESSION_STORAGE_KEY}${context.instanceId}`
);
if (selectedAirportInSessionStorage) {
setSelectedAirports([selectedAirportInSessionStorage]);
setAppState({
...appState,
selectedAirPort: (selectedAirportInSessionStorage as ITagExtended)?.airportData,
});
}
}
}, [context]);
return (
<div>
<div ref={divRef} className={controlStyles.searchContainerStyles}>
<Label>Airport</Label>
<TagPicker
selectedItems={selectedAirport}
styles={selecteAirportPickerStyles}
resolveDelay={500}
@ -160,7 +180,6 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
}}
/>
</div>
</div>
);
};

View File

@ -19,12 +19,16 @@ export interface ISelectDateProps {}
export const SelectDate: React.FunctionComponent<ISelectDateProps> = (
props: React.PropsWithChildren<ISelectDateProps>
) => {
const { selectedDateStyle, textFieldStyles, labelDateStyles, labelTimeStyles } = useSelctedDateStyles();
const [appState, setAppState] = useRecoilState(globalState);
const { selectedDate, selectedTime } = appState;
const { selectedDate, selectedTime } = appState;
const onSelectDate = React.useCallback(
(date: Date | null | undefined) => {
setAppState({
...appState,
selectedDate: date,
@ -33,6 +37,9 @@ export const SelectDate: React.FunctionComponent<ISelectDateProps> = (
[appState, setAppState, selectedTime]
);
const datePickerRef = React.useRef<IDatePicker>(null);
return (
<>

View File

@ -14,6 +14,7 @@ import { useRecoilState } from 'recoil';
import { EInformationType } from '../../constants/EInformationType';
import { EInformationTypesIcons } from '../../constants/EInformationTypesIcons';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { globalState } from '../../recoil/atoms';
import { useSelectInformationStyles } from './useSelectInformationStyles';
@ -22,8 +23,11 @@ export interface ISelectInformationTypeProps {}
export const SelectInformationType: React.FunctionComponent<ISelectInformationTypeProps> = (
props: React.PropsWithChildren<ISelectInformationTypeProps>
) => {
const SELECTED_INFORMATION_TYPE_SESSION_STORAGE_KEY = "___selectedInformationType___";
const [getSelectedInfTypeFromSessionStorage, setSelectedInfTypeToSessionStorage] = useLocalStorage();
const [appState, setAppState] = useRecoilState(globalState);
const { dropdownStyles, controlStyles } = useSelectInformationStyles();
const { context } = appState;
const { dropdownStyles, controlStyles } = useSelectInformationStyles();
const options: IDropdownOption[] = React.useMemo(() => {
return [
{ key: "Header", text: "Options", itemType: DropdownMenuItemType.Header },
@ -79,6 +83,20 @@ export const SelectInformationType: React.FunctionComponent<ISelectInformationTy
);
}, []);
React.useEffect(() => {
if (context) {
const selectedInformationTypeInSessionStorage = getSelectedInfTypeFromSessionStorage(
`${SELECTED_INFORMATION_TYPE_SESSION_STORAGE_KEY}${context.instanceId}`
);
if (selectedInformationTypeInSessionStorage) {
setAppState( (prevState) => {
return {...prevState, selectedInformationType: selectedInformationTypeInSessionStorage};
});
}
}
}, [context ]);
return (
<>
<Dropdown
@ -89,9 +107,15 @@ export const SelectInformationType: React.FunctionComponent<ISelectInformationTy
onRenderOption={onRenderOption}
styles={dropdownStyles}
options={options}
onChange={(event, option) =>
setAppState({ ...appState, selectedInformationType: option.key as EInformationType })
}
onChange={(event, option) => {
if (option) {
setAppState( (prevState) => { return {...prevState, selectedInformationType: option.key as EInformationType};});
setSelectedInfTypeToSessionStorage(
`${SELECTED_INFORMATION_TYPE_SESSION_STORAGE_KEY}${context.instanceId}`,
option.key
);
}
}}
selectedKey={appState.selectedInformationType}
/>
</>

View File

@ -9,21 +9,19 @@ import {
import { IAirlines } from '../models/IAirlines';
import { useLocalStorage } from './useLocalStorage';
const airlinesData = require("../mockData/airlines.json");
const airlinesData = require("../mockData/airlines.json");
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/no-floating-promises */
export const useAirlines = () => {
const [error, setError] = useState<Error>(null);
const [loading, setLoading] = useState(false);
const [airlines, setAirlines] = useState<IAirlines >({} as IAirlines);
const [airlines, setAirlines] = useState<IAirlines>({} as IAirlines);
const [airlinesLocalStorage, setAirLinesLocalStorage] = useLocalStorage("__airlines__", []);
const [getAirLinesFromSessionStorage, setAirLinesToSessionStorage] = useLocalStorage();
const fetchAirlines = useCallback(async () => {
try {
setAirLinesLocalStorage(airlinesData);
setAirLinesToSessionStorage("__airlines__",airlinesData);
} catch (error) {
if (DEBUG) {
console.log("[useAirLines] error", error);
@ -36,12 +34,13 @@ export const useAirlines = () => {
useEffect(() => {
(async () => {
setLoading(true);
if (!airlinesLocalStorage?.rows?.length ) {
const airlinesFromSessionStorage = getAirLinesFromSessionStorage("__airlines__");
if (!airlinesFromSessionStorage?.rows?.length) {
await fetchAirlines();
setAirlines(airlinesData);
setError(undefined);
} else {
setAirlines(airlinesLocalStorage);
setAirlines(airlinesFromSessionStorage);
}
setLoading(false);

View File

@ -1,36 +1,30 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import * as React from 'react';
import addSeconds from 'date-fns/addSeconds';
import isAfter from 'date-fns/isAfter';
/* eslint-disable @typescript-eslint/explicit-function-return-type */
interface IStorage {
value: unknown;
expires?: Date;
}
const DEFAULT_EXPIRED_IN_SECONDS = 60 * 60 * 1000; // 1 hour
const DEFAULT_EXPIRED_IN_SECONDS = 60 * 60; // 1 hour
const getStorageValue = (key:string, defaultValue:unknown):any => {
const storage:IStorage = JSON.parse(sessionStorage.getItem(key) || '{}');
// getting stored value
const { value, expires } = storage || {} as IStorage;
if (expires > new Date() ) {
return value || defaultValue;
}
return undefined ;
}
export const useLocalStorage = (): any => {
const setStorageValue = (key: string, newValue: unknown, expiredInSeconds: number) => {
const expires = addSeconds(new Date(), expiredInSeconds ?? DEFAULT_EXPIRED_IN_SECONDS);
sessionStorage.setItem(key, JSON.stringify({ value: newValue, expires }));
};
const getStorageValue = (key: string): any => {
const storage: IStorage = JSON.parse(sessionStorage.getItem(key) || "{}");
// getting stored value
const { value, expires } = storage || ({} as IStorage);
if (isAfter(new Date(expires), new Date())) {
return value;
}
return undefined;
};
export const useLocalStorage = (key:string, defaultValue:unknown, expiredIn?: number):any => {
const [value, setStorageValue] = React.useState(() => {
return getStorageValue(key, defaultValue);
});
React.useEffect(() => {
// save value
const expiredInValue = expiredIn ? new Date(new Date().getTime() + expiredIn * 1000) : DEFAULT_EXPIRED_IN_SECONDS;
sessionStorage.setItem(key, JSON.stringify({ value, expires:expiredInValue }));
}, [key, value, expiredIn]);
return [value, setStorageValue];
return [getStorageValue, setStorageValue];
};

View File

@ -18,4 +18,5 @@ export interface IGlobalState {
isScrolling: boolean;
hasMore: boolean;
webpartContainerWidth: number;
}

View File

@ -19,5 +19,6 @@ export const globalState = atom<IGlobalState>({
isScrolling: false,
hasMore: true,
webpartContainerWidth: 0
},
});

View File

@ -14,7 +14,6 @@ import {
IPropertyPaneConfiguration,
PropertyPaneSlider,
PropertyPaneTextField,
PropertyPaneToggle,
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
@ -34,18 +33,14 @@ export interface IFlightTrackerWebPartProps {
displayMode: DisplayMode;
updateProperty: (value: string) => void;
numberItemsPerPage: number;
refreshInterval: number;
enableRefreshInterval: boolean;
}
export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightTrackerWebPartProps> {
private _isDarkTheme: boolean = false;
private containerWidth: number = 0;
private _currentTheme: IReadonlyTheme | undefined;
// Apply Teams Context
private _applyTheme = (theme: string): void => {
this.context.domElement.setAttribute("data-theme", theme);
@ -53,67 +48,63 @@ export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightT
if (theme === "dark") {
loadTheme({
palette: teamsDarkTheme
palette: teamsDarkTheme,
});
}
if (theme === "default") {
loadTheme({
palette: teamsDefaultTheme
palette: teamsDefaultTheme,
});
}
if (theme === "contrast") {
loadTheme({
palette: teamsContrastTheme
palette: teamsContrastTheme,
});
}
}
};
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected onAfterResize(newWidth: number): void {
console.log("onAfterResize", newWidth);
this.containerWidth = newWidth;
this.render();
this.containerWidth = newWidth;
this.render();
}
public render(): void {
const element: React.ReactElement<IFlightTrackerProps> = React.createElement(
FlightTracker,
{
title: this.properties.title,
isDarkTheme: this._isDarkTheme,
context: this.context,
hasTeamsContext: !!this.context.sdks.microsoftTeams,
currentTheme: this._currentTheme ,
displayMode: this.displayMode,
numberItemsPerPage: this.properties.numberItemsPerPage,
updateProperty: (value: string) => {
this.properties.title = value;
},
webpartContainerWidth: this.containerWidth
}
);
const element: React.ReactElement<IFlightTrackerProps> = React.createElement(FlightTracker, {
title: this.properties.title,
isDarkTheme: this._isDarkTheme,
context: this.context,
hasTeamsContext: !!this.context.sdks.microsoftTeams,
currentTheme: this._currentTheme,
displayMode: this.displayMode,
numberItemsPerPage: this.properties.numberItemsPerPage,
updateProperty: (value: string) => {
this.properties.title = value;
},
webpartContainerWidth: this.containerWidth
});
ReactDom.render(element, this.domElement);
}
protected onInit(): Promise<void> {
if (this.context.sdks.microsoftTeams ) {
if (this.context.sdks.microsoftTeams) {
// in teams ?
const teamsContext = this.context.sdks.microsoftTeams?.context;
this._applyTheme(teamsContext.theme || "default");
this.context.sdks.microsoftTeams.teamsJs.registerOnThemeChangeHandler(
this._applyTheme
);
this.context.sdks.microsoftTeams.teamsJs.registerOnThemeChangeHandler(this._applyTheme);
}
this.containerWidth = this.domElement.clientWidth;
return super.onInit();
}
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
if (!currentTheme) {
return;
@ -127,7 +118,7 @@ export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightT
}
protected get dataVersion(): Version {
return Version.parse('1.0');
return Version.parse("1.0");
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
@ -135,40 +126,28 @@ export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightT
pages: [
{
header: {
description: strings.PropertyPaneDescription
description: strings.PropertyPaneDescription,
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('title', {
label: strings.DescriptionFieldLabel
PropertyPaneTextField("title", {
label: strings.DescriptionFieldLabel,
}),
PropertyPaneSlider('numberItemsPerPage', {
PropertyPaneSlider("numberItemsPerPage", {
label: strings.NumberItemsPerPageLabel,
value: this.properties.numberItemsPerPage,
min: 1,
min: 5,
max: 20,
showValue: true,
}),
PropertyPaneToggle('enableRefreshInterval', {
label: strings.RefreshIntervalLabel,
checked: this.properties.enableRefreshInterval,
offText: strings.RefreshIntervalOffText,
onText: strings.RefreshIntervalOnText,
}),
PropertyPaneSlider('refreshInterval', {
label: strings.RefreshIntervalLabel,
value: this.properties.refreshInterval,
min: 1,
max: 5,
showValue: true,
})
]
}
]
}
]
],
},
],
},
],
};
}
}