forked from vergnet/application-amicale
Update Amicale and related components to use TypeScript
This commit is contained in:
parent
18ec6e0a59
commit
f95635136e
17 changed files with 400 additions and 476 deletions
|
@ -17,37 +17,34 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {StackNavigationProp} from '@react-navigation/stack';
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import ConnectionManager from '../../managers/ConnectionManager';
|
import ConnectionManager from '../../managers/ConnectionManager';
|
||||||
import type {ApiGenericDataType} from '../../utils/WebData';
|
|
||||||
import {ERROR_TYPE} from '../../utils/WebData';
|
import {ERROR_TYPE} from '../../utils/WebData';
|
||||||
import ErrorView from '../Screens/ErrorView';
|
import ErrorView from '../Screens/ErrorView';
|
||||||
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
|
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType<T> = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp<any>;
|
||||||
requests: Array<{
|
requests: Array<{
|
||||||
link: string,
|
link: string;
|
||||||
params: {...},
|
params: object;
|
||||||
mandatory: boolean,
|
mandatory: boolean;
|
||||||
}>,
|
}>;
|
||||||
renderFunction: (Array<ApiGenericDataType | null>) => React.Node,
|
renderFunction: (data: Array<T | null>) => React.ReactNode;
|
||||||
errorViewOverride?: Array<{
|
errorViewOverride?: Array<{
|
||||||
errorCode: number,
|
errorCode: number;
|
||||||
message: string,
|
message: string;
|
||||||
icon: string,
|
icon: string;
|
||||||
showRetryButton: boolean,
|
showRetryButton: boolean;
|
||||||
}> | null,
|
}> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateType = {
|
type StateType = {
|
||||||
loading: boolean,
|
loading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
class AuthenticatedScreen extends React.Component<PropsType, StateType> {
|
class AuthenticatedScreen<T> extends React.Component<PropsType<T>, StateType> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
errorViewOverride: null,
|
errorViewOverride: null,
|
||||||
};
|
};
|
||||||
|
@ -58,13 +55,14 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
|
||||||
|
|
||||||
errors: Array<number>;
|
errors: Array<number>;
|
||||||
|
|
||||||
fetchedData: Array<ApiGenericDataType | null>;
|
fetchedData: Array<T | null>;
|
||||||
|
|
||||||
constructor(props: PropsType) {
|
constructor(props: PropsType<T>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: true,
|
loading: true,
|
||||||
};
|
};
|
||||||
|
this.currentUserToken = null;
|
||||||
this.connectionManager = ConnectionManager.getInstance();
|
this.connectionManager = ConnectionManager.getInstance();
|
||||||
props.navigation.addListener('focus', this.onScreenFocus);
|
props.navigation.addListener('focus', this.onScreenFocus);
|
||||||
this.fetchedData = new Array(props.requests.length);
|
this.fetchedData = new Array(props.requests.length);
|
||||||
|
@ -91,20 +89,20 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
|
||||||
* @param index The index for the data
|
* @param index The index for the data
|
||||||
* @param error The error code received
|
* @param error The error code received
|
||||||
*/
|
*/
|
||||||
onRequestFinished(
|
onRequestFinished(data: T | null, index: number, error?: number) {
|
||||||
data: ApiGenericDataType | null,
|
|
||||||
index: number,
|
|
||||||
error?: number,
|
|
||||||
) {
|
|
||||||
const {props} = this;
|
const {props} = this;
|
||||||
if (index >= 0 && index < props.requests.length) {
|
if (index >= 0 && index < props.requests.length) {
|
||||||
this.fetchedData[index] = data;
|
this.fetchedData[index] = data;
|
||||||
this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS;
|
this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS;
|
||||||
}
|
}
|
||||||
// Token expired, logout user
|
// Token expired, logout user
|
||||||
if (error === ERROR_TYPE.BAD_TOKEN) this.connectionManager.disconnect();
|
if (error === ERROR_TYPE.BAD_TOKEN) {
|
||||||
|
this.connectionManager.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.allRequestsFinished()) this.setState({loading: false});
|
if (this.allRequestsFinished()) {
|
||||||
|
this.setState({loading: false});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,7 +130,7 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
|
||||||
*
|
*
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getErrorRender(): React.Node {
|
getErrorRender() {
|
||||||
const {props} = this;
|
const {props} = this;
|
||||||
const errorCode = this.getError();
|
const errorCode = this.getError();
|
||||||
let shouldOverride = false;
|
let shouldOverride = false;
|
||||||
|
@ -169,18 +167,18 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
|
||||||
*/
|
*/
|
||||||
fetchData = () => {
|
fetchData = () => {
|
||||||
const {state, props} = this;
|
const {state, props} = this;
|
||||||
if (!state.loading) this.setState({loading: true});
|
if (!state.loading) {
|
||||||
|
this.setState({loading: true});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.connectionManager.isLoggedIn()) {
|
if (this.connectionManager.isLoggedIn()) {
|
||||||
for (let i = 0; i < props.requests.length; i += 1) {
|
for (let i = 0; i < props.requests.length; i += 1) {
|
||||||
this.connectionManager
|
this.connectionManager
|
||||||
.authenticatedRequest(
|
.authenticatedRequest<T>(
|
||||||
props.requests[i].link,
|
props.requests[i].link,
|
||||||
props.requests[i].params,
|
props.requests[i].params,
|
||||||
)
|
)
|
||||||
.then((response: ApiGenericDataType): void =>
|
.then((response: T): void => this.onRequestFinished(response, i))
|
||||||
this.onRequestFinished(response, i),
|
|
||||||
)
|
|
||||||
.catch((error: number): void =>
|
.catch((error: number): void =>
|
||||||
this.onRequestFinished(null, i, error),
|
this.onRequestFinished(null, i, error),
|
||||||
);
|
);
|
||||||
|
@ -200,7 +198,9 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
|
||||||
allRequestsFinished(): boolean {
|
allRequestsFinished(): boolean {
|
||||||
let finished = true;
|
let finished = true;
|
||||||
this.errors.forEach((error: number | null) => {
|
this.errors.forEach((error: number | null) => {
|
||||||
if (error == null) finished = false;
|
if (error == null) {
|
||||||
|
finished = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return finished;
|
return finished;
|
||||||
}
|
}
|
||||||
|
@ -212,11 +212,14 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): React.Node {
|
render() {
|
||||||
const {state, props} = this;
|
const {state, props} = this;
|
||||||
if (state.loading) return <BasicLoadingScreen />;
|
if (state.loading) {
|
||||||
if (this.getError() === ERROR_TYPE.SUCCESS)
|
return <BasicLoadingScreen />;
|
||||||
|
}
|
||||||
|
if (this.getError() === ERROR_TYPE.SUCCESS) {
|
||||||
return props.renderFunction(this.fetchedData);
|
return props.renderFunction(this.fetchedData);
|
||||||
|
}
|
||||||
return this.getErrorRender();
|
return this.getErrorRender();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -17,28 +17,25 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import {StackNavigationProp} from '@react-navigation/stack';
|
|
||||||
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
|
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
|
||||||
import ConnectionManager from '../../managers/ConnectionManager';
|
import ConnectionManager from '../../managers/ConnectionManager';
|
||||||
|
import {useNavigation} from '@react-navigation/native';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
visible: boolean;
|
||||||
visible: boolean,
|
onDismiss: () => void;
|
||||||
onDismiss: () => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class LogoutDialog extends React.PureComponent<PropsType> {
|
function LogoutDialog(props: PropsType) {
|
||||||
onClickAccept = async (): Promise<void> => {
|
const navigation = useNavigation();
|
||||||
const {props} = this;
|
const onClickAccept = async (): Promise<void> => {
|
||||||
return new Promise((resolve: () => void) => {
|
return new Promise((resolve: () => void) => {
|
||||||
ConnectionManager.getInstance()
|
ConnectionManager.getInstance()
|
||||||
.disconnect()
|
.disconnect()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
props.navigation.reset({
|
navigation.reset({
|
||||||
index: 0,
|
index: 0,
|
||||||
routes: [{name: 'main'}],
|
routes: [{name: 'main'}],
|
||||||
});
|
});
|
||||||
|
@ -48,19 +45,16 @@ class LogoutDialog extends React.PureComponent<PropsType> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render(): React.Node {
|
|
||||||
const {props} = this;
|
|
||||||
return (
|
return (
|
||||||
<LoadingConfirmDialog
|
<LoadingConfirmDialog
|
||||||
visible={props.visible}
|
visible={props.visible}
|
||||||
onDismiss={props.onDismiss}
|
onDismiss={props.onDismiss}
|
||||||
onAccept={this.onClickAccept}
|
onAccept={onClickAccept}
|
||||||
title={i18n.t('dialog.disconnect.title')}
|
title={i18n.t('dialog.disconnect.title')}
|
||||||
titleLoading={i18n.t('dialog.disconnect.titleLoading')}
|
titleLoading={i18n.t('dialog.disconnect.titleLoading')}
|
||||||
message={i18n.t('dialog.disconnect.message')}
|
message={i18n.t('dialog.disconnect.message')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default LogoutDialog;
|
export default LogoutDialog;
|
|
@ -17,25 +17,13 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
import React from 'react';
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import {View} from 'react-native';
|
import {View} from 'react-native';
|
||||||
import {Headline, withTheme} from 'react-native-paper';
|
import {Headline, useTheme} from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
|
||||||
|
|
||||||
type PropsType = {
|
function VoteNotAvailable() {
|
||||||
theme: CustomThemeType,
|
const theme = useTheme();
|
||||||
};
|
|
||||||
|
|
||||||
class VoteNotAvailable extends React.Component<PropsType> {
|
|
||||||
shouldComponentUpdate(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(): React.Node {
|
|
||||||
const {props} = this;
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
@ -45,7 +33,7 @@ class VoteNotAvailable extends React.Component<PropsType> {
|
||||||
}}>
|
}}>
|
||||||
<Headline
|
<Headline
|
||||||
style={{
|
style={{
|
||||||
color: props.theme.colors.textDisabled,
|
color: theme.colors.textDisabled,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}>
|
}}>
|
||||||
{i18n.t('screens.vote.noVote')}
|
{i18n.t('screens.vote.noVote')}
|
||||||
|
@ -53,6 +41,5 @@ class VoteNotAvailable extends React.Component<PropsType> {
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default withTheme(VoteNotAvailable);
|
export default VoteNotAvailable;
|
|
@ -17,8 +17,6 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
@ -31,16 +29,11 @@ import {
|
||||||
import {FlatList, StyleSheet} from 'react-native';
|
import {FlatList, StyleSheet} from 'react-native';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
|
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
|
||||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
|
||||||
import type {
|
|
||||||
CardTitleIconPropsType,
|
|
||||||
ListIconPropsType,
|
|
||||||
} from '../../../constants/PaperStyles';
|
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
teams: Array<VoteTeamType>,
|
teams: Array<VoteTeamType>;
|
||||||
dateEnd: string,
|
dateEnd: string;
|
||||||
theme: CustomThemeType,
|
theme: ReactNativePaper.Theme;
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
@ -58,10 +51,10 @@ class VoteResults extends React.Component<PropsType> {
|
||||||
winnerIds: Array<number>;
|
winnerIds: Array<number>;
|
||||||
|
|
||||||
constructor(props: PropsType) {
|
constructor(props: PropsType) {
|
||||||
super();
|
super(props);
|
||||||
props.teams.sort(this.sortByVotes);
|
props.teams.sort(this.sortByVotes);
|
||||||
this.getTotalVotes(props.teams);
|
this.totalVotes = this.getTotalVotes(props.teams);
|
||||||
this.getWinnerIds(props.teams);
|
this.winnerIds = this.getWinnerIds(props.teams);
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(): boolean {
|
shouldComponentUpdate(): boolean {
|
||||||
|
@ -69,26 +62,31 @@ class VoteResults extends React.Component<PropsType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalVotes(teams: Array<VoteTeamType>) {
|
getTotalVotes(teams: Array<VoteTeamType>) {
|
||||||
this.totalVotes = 0;
|
let totalVotes = 0;
|
||||||
for (let i = 0; i < teams.length; i += 1) {
|
for (let i = 0; i < teams.length; i += 1) {
|
||||||
this.totalVotes += teams[i].votes;
|
totalVotes += teams[i].votes;
|
||||||
}
|
}
|
||||||
|
return totalVotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
getWinnerIds(teams: Array<VoteTeamType>) {
|
getWinnerIds(teams: Array<VoteTeamType>) {
|
||||||
const max = teams[0].votes;
|
const max = teams[0].votes;
|
||||||
this.winnerIds = [];
|
let winnerIds = [];
|
||||||
for (let i = 0; i < teams.length; i += 1) {
|
for (let i = 0; i < teams.length; i += 1) {
|
||||||
if (teams[i].votes === max) this.winnerIds.push(teams[i].id);
|
if (teams[i].votes === max) {
|
||||||
else break;
|
winnerIds.push(teams[i].id);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return winnerIds;
|
||||||
|
}
|
||||||
|
|
||||||
sortByVotes = (a: VoteTeamType, b: VoteTeamType): number => b.votes - a.votes;
|
sortByVotes = (a: VoteTeamType, b: VoteTeamType): number => b.votes - a.votes;
|
||||||
|
|
||||||
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
||||||
|
|
||||||
resultRenderItem = ({item}: {item: VoteTeamType}): React.Node => {
|
resultRenderItem = ({item}: {item: VoteTeamType}) => {
|
||||||
const isWinner = this.winnerIds.indexOf(item.id) !== -1;
|
const isWinner = this.winnerIds.indexOf(item.id) !== -1;
|
||||||
const isDraw = this.winnerIds.length > 1;
|
const isDraw = this.winnerIds.length > 1;
|
||||||
const {props} = this;
|
const {props} = this;
|
||||||
|
@ -101,7 +99,7 @@ class VoteResults extends React.Component<PropsType> {
|
||||||
<List.Item
|
<List.Item
|
||||||
title={item.name}
|
title={item.name}
|
||||||
description={`${item.votes} ${i18n.t('screens.vote.results.votes')}`}
|
description={`${item.votes} ${i18n.t('screens.vote.results.votes')}`}
|
||||||
left={(iconProps: ListIconPropsType): React.Node =>
|
left={(iconProps) =>
|
||||||
isWinner ? (
|
isWinner ? (
|
||||||
<List.Icon
|
<List.Icon
|
||||||
style={iconProps.style}
|
style={iconProps.style}
|
||||||
|
@ -125,7 +123,7 @@ class VoteResults extends React.Component<PropsType> {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render(): React.Node {
|
render() {
|
||||||
const {props} = this;
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<Card style={styles.card}>
|
<Card style={styles.card}>
|
||||||
|
@ -134,15 +132,14 @@ class VoteResults extends React.Component<PropsType> {
|
||||||
subtitle={`${i18n.t('screens.vote.results.subtitle')} ${
|
subtitle={`${i18n.t('screens.vote.results.subtitle')} ${
|
||||||
props.dateEnd
|
props.dateEnd
|
||||||
}`}
|
}`}
|
||||||
left={(iconProps: CardTitleIconPropsType): React.Node => (
|
left={(iconProps) => (
|
||||||
<Avatar.Icon size={iconProps.size} icon="podium-gold" />
|
<Avatar.Icon size={iconProps.size} icon="podium-gold" />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<Subheading>{`${i18n.t('screens.vote.results.totalVotes')} ${
|
<Subheading>
|
||||||
this.totalVotes
|
{`${i18n.t('screens.vote.results.totalVotes')} ${this.totalVotes}`}
|
||||||
}`}</Subheading>
|
</Subheading>
|
||||||
{/* $FlowFixMe */}
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={props.teams}
|
data={props.teams}
|
||||||
keyExtractor={this.voteKeyExtractor}
|
keyExtractor={this.voteKeyExtractor}
|
|
@ -17,8 +17,6 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Button, Card, RadioButton} from 'react-native-paper';
|
import {Avatar, Button, Card, RadioButton} from 'react-native-paper';
|
||||||
import {FlatList, StyleSheet, View} from 'react-native';
|
import {FlatList, StyleSheet, View} from 'react-native';
|
||||||
|
@ -27,19 +25,18 @@ import ConnectionManager from '../../../managers/ConnectionManager';
|
||||||
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
|
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
|
||||||
import ErrorDialog from '../../Dialogs/ErrorDialog';
|
import ErrorDialog from '../../Dialogs/ErrorDialog';
|
||||||
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
|
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
|
||||||
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
|
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
teams: Array<VoteTeamType>,
|
teams: Array<VoteTeamType>;
|
||||||
onVoteSuccess: () => void,
|
onVoteSuccess: () => void;
|
||||||
onVoteError: () => void,
|
onVoteError: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateType = {
|
type StateType = {
|
||||||
selectedTeam: string,
|
selectedTeam: string;
|
||||||
voteDialogVisible: boolean,
|
voteDialogVisible: boolean;
|
||||||
errorDialogVisible: boolean,
|
errorDialogVisible: boolean;
|
||||||
currentError: number,
|
currentError: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
@ -53,10 +50,10 @@ const styles = StyleSheet.create({
|
||||||
|
|
||||||
export default class VoteSelect extends React.PureComponent<
|
export default class VoteSelect extends React.PureComponent<
|
||||||
PropsType,
|
PropsType,
|
||||||
StateType,
|
StateType
|
||||||
> {
|
> {
|
||||||
constructor() {
|
constructor(props: PropsType) {
|
||||||
super();
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
selectedTeam: 'none',
|
selectedTeam: 'none',
|
||||||
voteDialogVisible: false,
|
voteDialogVisible: false,
|
||||||
|
@ -70,7 +67,7 @@ export default class VoteSelect extends React.PureComponent<
|
||||||
|
|
||||||
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
||||||
|
|
||||||
voteRenderItem = ({item}: {item: VoteTeamType}): React.Node => (
|
voteRenderItem = ({item}: {item: VoteTeamType}) => (
|
||||||
<RadioButton.Item label={item.name} value={item.id.toString()} />
|
<RadioButton.Item label={item.name} value={item.id.toString()} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -111,7 +108,7 @@ export default class VoteSelect extends React.PureComponent<
|
||||||
props.onVoteError();
|
props.onVoteError();
|
||||||
};
|
};
|
||||||
|
|
||||||
render(): React.Node {
|
render() {
|
||||||
const {state, props} = this;
|
const {state, props} = this;
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
|
@ -119,7 +116,7 @@ export default class VoteSelect extends React.PureComponent<
|
||||||
<Card.Title
|
<Card.Title
|
||||||
title={i18n.t('screens.vote.select.title')}
|
title={i18n.t('screens.vote.select.title')}
|
||||||
subtitle={i18n.t('screens.vote.select.subtitle')}
|
subtitle={i18n.t('screens.vote.select.subtitle')}
|
||||||
left={(iconProps: CardTitleIconPropsType): React.Node => (
|
left={(iconProps) => (
|
||||||
<Avatar.Icon size={iconProps.size} icon="alert-decagram" />
|
<Avatar.Icon size={iconProps.size} icon="alert-decagram" />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -127,7 +124,6 @@ export default class VoteSelect extends React.PureComponent<
|
||||||
<RadioButton.Group
|
<RadioButton.Group
|
||||||
onValueChange={this.onVoteSelectionChange}
|
onValueChange={this.onVoteSelectionChange}
|
||||||
value={state.selectedTeam}>
|
value={state.selectedTeam}>
|
||||||
{/* $FlowFixMe */}
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={props.teams}
|
data={props.teams}
|
||||||
keyExtractor={this.voteKeyExtractor}
|
keyExtractor={this.voteKeyExtractor}
|
|
@ -17,16 +17,13 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Card, Paragraph} from 'react-native-paper';
|
import {Avatar, Card, Paragraph} from 'react-native-paper';
|
||||||
import {StyleSheet} from 'react-native';
|
import {StyleSheet} from 'react-native';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
|
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
startDate: string,
|
startDate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
@ -38,21 +35,13 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class VoteTease extends React.Component<PropsType> {
|
export default function VoteTease(props: PropsType) {
|
||||||
shouldComponentUpdate(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(): React.Node {
|
|
||||||
const {props} = this;
|
|
||||||
return (
|
return (
|
||||||
<Card style={styles.card}>
|
<Card style={styles.card}>
|
||||||
<Card.Title
|
<Card.Title
|
||||||
title={i18n.t('screens.vote.tease.title')}
|
title={i18n.t('screens.vote.tease.title')}
|
||||||
subtitle={i18n.t('screens.vote.tease.subtitle')}
|
subtitle={i18n.t('screens.vote.tease.subtitle')}
|
||||||
left={(iconProps: CardTitleIconPropsType): React.Node => (
|
left={(iconProps) => <Avatar.Icon size={iconProps.size} icon="vote" />}
|
||||||
<Avatar.Icon size={iconProps.size} icon="vote" />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
|
@ -62,4 +51,3 @@ export default class VoteTease extends React.Component<PropsType> {
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
|
|
||||||
*
|
|
||||||
* This file is part of Campus INSAT.
|
|
||||||
*
|
|
||||||
* Campus INSAT is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Campus INSAT is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import {Avatar, Card, Paragraph, withTheme} from 'react-native-paper';
|
|
||||||
import {StyleSheet} from 'react-native';
|
|
||||||
import i18n from 'i18n-js';
|
|
||||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
|
||||||
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
|
|
||||||
|
|
||||||
type PropsType = {
|
|
||||||
startDate: string | null,
|
|
||||||
justVoted: boolean,
|
|
||||||
hasVoted: boolean,
|
|
||||||
isVoteRunning: boolean,
|
|
||||||
theme: CustomThemeType,
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
card: {
|
|
||||||
margin: 10,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
class VoteWait extends React.Component<PropsType> {
|
|
||||||
shouldComponentUpdate(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(): React.Node {
|
|
||||||
const {props} = this;
|
|
||||||
const {startDate} = props;
|
|
||||||
return (
|
|
||||||
<Card style={styles.card}>
|
|
||||||
<Card.Title
|
|
||||||
title={
|
|
||||||
props.isVoteRunning
|
|
||||||
? i18n.t('screens.vote.wait.titleSubmitted')
|
|
||||||
: i18n.t('screens.vote.wait.titleEnded')
|
|
||||||
}
|
|
||||||
subtitle={i18n.t('screens.vote.wait.subtitle')}
|
|
||||||
left={(iconProps: CardTitleIconPropsType): React.Node => (
|
|
||||||
<Avatar.Icon size={iconProps.size} icon="progress-check" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Card.Content>
|
|
||||||
{props.justVoted ? (
|
|
||||||
<Paragraph style={{color: props.theme.colors.success}}>
|
|
||||||
{i18n.t('screens.vote.wait.messageSubmitted')}
|
|
||||||
</Paragraph>
|
|
||||||
) : null}
|
|
||||||
{props.hasVoted ? (
|
|
||||||
<Paragraph style={{color: props.theme.colors.success}}>
|
|
||||||
{i18n.t('screens.vote.wait.messageVoted')}
|
|
||||||
</Paragraph>
|
|
||||||
) : null}
|
|
||||||
{startDate != null ? (
|
|
||||||
<Paragraph>
|
|
||||||
{`${i18n.t('screens.vote.wait.messageDate')} ${startDate}`}
|
|
||||||
</Paragraph>
|
|
||||||
) : (
|
|
||||||
<Paragraph>
|
|
||||||
{i18n.t('screens.vote.wait.messageDateUndefined')}
|
|
||||||
</Paragraph>
|
|
||||||
)}
|
|
||||||
</Card.Content>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTheme(VoteWait);
|
|
80
src/components/Amicale/Vote/VoteWait.tsx
Normal file
80
src/components/Amicale/Vote/VoteWait.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
|
||||||
|
*
|
||||||
|
* This file is part of Campus INSAT.
|
||||||
|
*
|
||||||
|
* Campus INSAT is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Campus INSAT is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import {Avatar, Card, Paragraph, useTheme} from 'react-native-paper';
|
||||||
|
import {StyleSheet} from 'react-native';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
|
||||||
|
type PropsType = {
|
||||||
|
startDate: string | null;
|
||||||
|
justVoted: boolean;
|
||||||
|
hasVoted: boolean;
|
||||||
|
isVoteRunning: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
margin: 10,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function VoteWait(props: PropsType) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const {startDate} = props;
|
||||||
|
return (
|
||||||
|
<Card style={styles.card}>
|
||||||
|
<Card.Title
|
||||||
|
title={
|
||||||
|
props.isVoteRunning
|
||||||
|
? i18n.t('screens.vote.wait.titleSubmitted')
|
||||||
|
: i18n.t('screens.vote.wait.titleEnded')
|
||||||
|
}
|
||||||
|
subtitle={i18n.t('screens.vote.wait.subtitle')}
|
||||||
|
left={(iconProps) => (
|
||||||
|
<Avatar.Icon size={iconProps.size} icon="progress-check" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Card.Content>
|
||||||
|
{props.justVoted ? (
|
||||||
|
<Paragraph style={{color: theme.colors.success}}>
|
||||||
|
{i18n.t('screens.vote.wait.messageSubmitted')}
|
||||||
|
</Paragraph>
|
||||||
|
) : null}
|
||||||
|
{props.hasVoted ? (
|
||||||
|
<Paragraph style={{color: theme.colors.success}}>
|
||||||
|
{i18n.t('screens.vote.wait.messageVoted')}
|
||||||
|
</Paragraph>
|
||||||
|
) : null}
|
||||||
|
{startDate != null ? (
|
||||||
|
<Paragraph>
|
||||||
|
{`${i18n.t('screens.vote.wait.messageDate')} ${startDate}`}
|
||||||
|
</Paragraph>
|
||||||
|
) : (
|
||||||
|
<Paragraph>
|
||||||
|
{i18n.t('screens.vote.wait.messageDateUndefined')}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,78 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
|
|
||||||
*
|
|
||||||
* This file is part of Campus INSAT.
|
|
||||||
*
|
|
||||||
* Campus INSAT is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Campus INSAT is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import {Collapsible} from 'react-navigation-collapsible';
|
|
||||||
import withCollapsible from '../../utils/withCollapsible';
|
|
||||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
|
||||||
|
|
||||||
export type CollapsibleComponentPropsType = {
|
|
||||||
children?: React.Node,
|
|
||||||
hasTab?: boolean,
|
|
||||||
onScroll?: (event: SyntheticEvent<EventTarget>) => void,
|
|
||||||
};
|
|
||||||
|
|
||||||
type PropsType = {
|
|
||||||
...CollapsibleComponentPropsType,
|
|
||||||
collapsibleStack: Collapsible,
|
|
||||||
// eslint-disable-next-line flowtype/no-weak-types
|
|
||||||
component: any,
|
|
||||||
};
|
|
||||||
|
|
||||||
class CollapsibleComponent extends React.Component<PropsType> {
|
|
||||||
static defaultProps = {
|
|
||||||
children: null,
|
|
||||||
hasTab: false,
|
|
||||||
onScroll: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
onScroll = (event: SyntheticEvent<EventTarget>) => {
|
|
||||||
const {props} = this;
|
|
||||||
if (props.onScroll) props.onScroll(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
render(): React.Node {
|
|
||||||
const {props} = this;
|
|
||||||
const Comp = props.component;
|
|
||||||
const {
|
|
||||||
containerPaddingTop,
|
|
||||||
scrollIndicatorInsetTop,
|
|
||||||
onScrollWithListener,
|
|
||||||
} = props.collapsibleStack;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
||||||
{...props}
|
|
||||||
onScroll={onScrollWithListener(this.onScroll)}
|
|
||||||
contentContainerStyle={{
|
|
||||||
paddingTop: containerPaddingTop,
|
|
||||||
paddingBottom: props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0,
|
|
||||||
minHeight: '100%',
|
|
||||||
}}
|
|
||||||
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
|
|
||||||
{props.children}
|
|
||||||
</Comp>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withCollapsible(CollapsibleComponent);
|
|
63
src/components/Collapsible/CollapsibleComponent.tsx
Normal file
63
src/components/Collapsible/CollapsibleComponent.tsx
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
|
||||||
|
*
|
||||||
|
* This file is part of Campus INSAT.
|
||||||
|
*
|
||||||
|
* Campus INSAT is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Campus INSAT is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import {useCollapsibleStack} from 'react-navigation-collapsible';
|
||||||
|
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||||
|
import {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
|
||||||
|
|
||||||
|
export interface CollapsibleComponentPropsType {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
hasTab?: boolean;
|
||||||
|
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropsType extends CollapsibleComponentPropsType {
|
||||||
|
component: React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleComponent(props: PropsType) {
|
||||||
|
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
|
if (props.onScroll) {
|
||||||
|
props.onScroll(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const Comp = props.component;
|
||||||
|
const {
|
||||||
|
containerPaddingTop,
|
||||||
|
scrollIndicatorInsetTop,
|
||||||
|
onScrollWithListener,
|
||||||
|
} = useCollapsibleStack();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
{...props}
|
||||||
|
onScroll={onScrollWithListener(onScroll)}
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingTop: containerPaddingTop,
|
||||||
|
paddingBottom: props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0,
|
||||||
|
minHeight: '100%',
|
||||||
|
}}
|
||||||
|
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
|
||||||
|
{props.children}
|
||||||
|
</Comp>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CollapsibleComponent;
|
|
@ -17,29 +17,19 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Animated} from 'react-native';
|
import {Animated, FlatListProps} from 'react-native';
|
||||||
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||||
import CollapsibleComponent from './CollapsibleComponent';
|
import CollapsibleComponent from './CollapsibleComponent';
|
||||||
|
|
||||||
type PropsType = {
|
type Props<T> = FlatListProps<T> & CollapsibleComponentPropsType;
|
||||||
...CollapsibleComponentPropsType,
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/prefer-stateless-function
|
function CollapsibleFlatList<T>(props: Props<T>) {
|
||||||
class CollapsibleFlatList extends React.Component<PropsType> {
|
|
||||||
render(): React.Node {
|
|
||||||
const {props} = this;
|
|
||||||
return (
|
return (
|
||||||
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
|
<CollapsibleComponent {...props} component={Animated.FlatList}>
|
||||||
{...props}
|
|
||||||
component={Animated.FlatList}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default CollapsibleFlatList;
|
export default CollapsibleFlatList;
|
|
@ -17,29 +17,19 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Animated} from 'react-native';
|
import {Animated, ScrollViewProps} from 'react-native';
|
||||||
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||||
import CollapsibleComponent from './CollapsibleComponent';
|
import CollapsibleComponent from './CollapsibleComponent';
|
||||||
|
|
||||||
type PropsType = {
|
type Props = ScrollViewProps & CollapsibleComponentPropsType;
|
||||||
...CollapsibleComponentPropsType,
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/prefer-stateless-function
|
function CollapsibleScrollView(props: Props) {
|
||||||
class CollapsibleScrollView extends React.Component<PropsType> {
|
|
||||||
render(): React.Node {
|
|
||||||
const {props} = this;
|
|
||||||
return (
|
return (
|
||||||
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
|
<CollapsibleComponent {...props} component={Animated.ScrollView}>
|
||||||
{...props}
|
|
||||||
component={Animated.ScrollView}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default CollapsibleScrollView;
|
export default CollapsibleScrollView;
|
|
@ -17,29 +17,19 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Animated} from 'react-native';
|
import {Animated, SectionListProps} from 'react-native';
|
||||||
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||||
import CollapsibleComponent from './CollapsibleComponent';
|
import CollapsibleComponent from './CollapsibleComponent';
|
||||||
|
|
||||||
type PropsType = {
|
type Props<T> = SectionListProps<T> & CollapsibleComponentPropsType;
|
||||||
...CollapsibleComponentPropsType,
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/prefer-stateless-function
|
function CollapsibleSectionList<T>(props: Props<T>) {
|
||||||
class CollapsibleSectionList extends React.Component<PropsType> {
|
|
||||||
render(): React.Node {
|
|
||||||
const {props} = this;
|
|
||||||
return (
|
return (
|
||||||
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
|
<CollapsibleComponent {...props} component={Animated.SectionList}>
|
||||||
{...props}
|
|
||||||
component={Animated.SectionList}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default CollapsibleSectionList;
|
export default CollapsibleSectionList;
|
|
@ -21,8 +21,11 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {View} from 'react-native';
|
import {View} from 'react-native';
|
||||||
import {ActivityIndicator, withTheme} from 'react-native-paper';
|
import {ActivityIndicator, useTheme} from 'react-native-paper';
|
||||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
|
||||||
|
type Props = {
|
||||||
|
isAbsolute?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component used to display a header button
|
* Component used to display a header button
|
||||||
|
@ -30,29 +33,21 @@ import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
* @param props Props to pass to the component
|
* @param props Props to pass to the component
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
function BasicLoadingScreen(props: {
|
export default function BasicLoadingScreen(props: Props) {
|
||||||
theme: CustomThemeType,
|
const theme = useTheme();
|
||||||
isAbsolute: boolean,
|
const {isAbsolute} = props;
|
||||||
}): React.Node {
|
|
||||||
const {theme, isAbsolute} = props;
|
|
||||||
const {colors} = theme;
|
|
||||||
let position;
|
|
||||||
if (isAbsolute != null && isAbsolute) position = 'absolute';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
position,
|
position: isAbsolute ? 'absolute' : 'relative',
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}>
|
}}>
|
||||||
<ActivityIndicator animating size="large" color={colors.primary} />
|
<ActivityIndicator animating size="large" color={theme.colors.primary} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(BasicLoadingScreen);
|
|
|
@ -17,8 +17,6 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Button, Subheading, withTheme} from 'react-native-paper';
|
import {Button, Subheading, withTheme} from 'react-native-paper';
|
||||||
import {StyleSheet, View} from 'react-native';
|
import {StyleSheet, View} from 'react-native';
|
||||||
|
@ -27,17 +25,16 @@ import i18n from 'i18n-js';
|
||||||
import * as Animatable from 'react-native-animatable';
|
import * as Animatable from 'react-native-animatable';
|
||||||
import {StackNavigationProp} from '@react-navigation/stack';
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import {ERROR_TYPE} from '../../utils/WebData';
|
import {ERROR_TYPE} from '../../utils/WebData';
|
||||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation?: StackNavigationProp<any>;
|
||||||
theme: CustomThemeType,
|
theme: ReactNativePaper.Theme;
|
||||||
route: {name: string},
|
route?: {name: string};
|
||||||
onRefresh?: () => void,
|
onRefresh?: () => void;
|
||||||
errorCode?: number,
|
errorCode?: number;
|
||||||
icon?: string,
|
icon?: string;
|
||||||
message?: string,
|
message?: string;
|
||||||
showRetryButton?: boolean,
|
showRetryButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
@ -82,9 +79,11 @@ class ErrorView extends React.PureComponent<PropsType> {
|
||||||
constructor(props: PropsType) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
this.icon = '';
|
this.icon = '';
|
||||||
|
this.showLoginButton = false;
|
||||||
|
this.message = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getRetryButton(): React.Node {
|
getRetryButton() {
|
||||||
const {props} = this;
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -97,7 +96,7 @@ class ErrorView extends React.PureComponent<PropsType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getLoginButton(): React.Node {
|
getLoginButton() {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
|
@ -111,10 +110,12 @@ class ErrorView extends React.PureComponent<PropsType> {
|
||||||
|
|
||||||
goToLogin = () => {
|
goToLogin = () => {
|
||||||
const {props} = this;
|
const {props} = this;
|
||||||
|
if (props.navigation) {
|
||||||
props.navigation.navigate('login', {
|
props.navigation.navigate('login', {
|
||||||
screen: 'login',
|
screen: 'login',
|
||||||
params: {nextScreen: props.route.name},
|
params: {nextScreen: props.route ? props.route.name : undefined},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
generateMessage() {
|
generateMessage() {
|
||||||
|
@ -169,13 +170,17 @@ class ErrorView extends React.PureComponent<PropsType> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): React.Node {
|
render() {
|
||||||
const {props} = this;
|
const {props} = this;
|
||||||
this.generateMessage();
|
this.generateMessage();
|
||||||
let button;
|
let button;
|
||||||
if (this.showLoginButton) button = this.getLoginButton();
|
if (this.showLoginButton) {
|
||||||
else if (props.showRetryButton) button = this.getRetryButton();
|
button = this.getLoginButton();
|
||||||
else button = null;
|
} else if (props.showRetryButton) {
|
||||||
|
button = this.getRetryButton();
|
||||||
|
} else {
|
||||||
|
button = null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animatable.View
|
<Animatable.View
|
|
@ -17,8 +17,6 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @flow
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {RefreshControl, View} from 'react-native';
|
import {RefreshControl, View} from 'react-native';
|
||||||
import {StackNavigationProp} from '@react-navigation/stack';
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
|
@ -35,31 +33,30 @@ import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||||
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||||
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
|
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
|
||||||
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||||
import type {ApiGenericDataType} from '../../utils/WebData';
|
|
||||||
|
|
||||||
export type VoteTeamType = {
|
export type VoteTeamType = {
|
||||||
id: number,
|
id: number;
|
||||||
name: string,
|
name: string;
|
||||||
votes: number,
|
votes: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TeamResponseType = {
|
type TeamResponseType = {
|
||||||
has_voted: boolean,
|
has_voted: boolean;
|
||||||
teams: Array<VoteTeamType>,
|
teams: Array<VoteTeamType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VoteDatesStringType = {
|
type VoteDatesStringType = {
|
||||||
date_begin: string,
|
date_begin: string;
|
||||||
date_end: string,
|
date_end: string;
|
||||||
date_result_begin: string,
|
date_result_begin: string;
|
||||||
date_result_end: string,
|
date_result_end: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VoteDatesObjectType = {
|
type VoteDatesObjectType = {
|
||||||
date_begin: Date,
|
date_begin: Date;
|
||||||
date_end: Date,
|
date_end: Date;
|
||||||
date_result_begin: Date,
|
date_result_begin: Date;
|
||||||
date_result_end: Date,
|
date_result_end: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
// const FAKE_DATE = {
|
// const FAKE_DATE = {
|
||||||
|
@ -113,12 +110,12 @@ type VoteDatesObjectType = {
|
||||||
const MIN_REFRESH_TIME = 5 * 1000;
|
const MIN_REFRESH_TIME = 5 * 1000;
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateType = {
|
type StateType = {
|
||||||
hasVoted: boolean,
|
hasVoted: boolean;
|
||||||
mascotDialogVisible: boolean,
|
mascotDialogVisible: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,10 +136,13 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
|
|
||||||
lastRefresh: Date | null;
|
lastRefresh: Date | null;
|
||||||
|
|
||||||
authRef: {current: null | AuthenticatedScreen};
|
authRef: {current: null | AuthenticatedScreen<any>};
|
||||||
|
|
||||||
constructor() {
|
constructor(props: PropsType) {
|
||||||
super();
|
super(props);
|
||||||
|
this.teams = [];
|
||||||
|
this.datesString = null;
|
||||||
|
this.dates = null;
|
||||||
this.state = {
|
this.state = {
|
||||||
hasVoted: false,
|
hasVoted: false,
|
||||||
mascotDialogVisible: AsyncStorageManager.getBool(
|
mascotDialogVisible: AsyncStorageManager.getBool(
|
||||||
|
@ -174,8 +174,8 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
return dateString;
|
return dateString;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMainRenderItem = ({item}: {item: {key: string}}): React.Node => {
|
getMainRenderItem = ({item}: {item: {key: string}}) => {
|
||||||
if (item.key === 'info')
|
if (item.key === 'info') {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Button
|
<Button
|
||||||
|
@ -191,21 +191,24 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return this.getContent();
|
return this.getContent();
|
||||||
};
|
};
|
||||||
|
|
||||||
getScreen = (data: Array<ApiGenericDataType | null>): React.Node => {
|
getScreen = (data: Array<TeamResponseType | VoteDatesStringType | null>) => {
|
||||||
const {state} = this;
|
const {state} = this;
|
||||||
// data[0] = FAKE_TEAMS2;
|
// data[0] = FAKE_TEAMS2;
|
||||||
// data[1] = FAKE_DATE;
|
// data[1] = FAKE_DATE;
|
||||||
this.lastRefresh = new Date();
|
this.lastRefresh = new Date();
|
||||||
|
|
||||||
const teams: TeamResponseType | null = data[0];
|
const teams = data[0] as TeamResponseType | null;
|
||||||
const dateStrings: VoteDatesStringType | null = data[1];
|
const dateStrings = data[1] as VoteDatesStringType | null;
|
||||||
|
|
||||||
if (dateStrings != null && dateStrings.date_begin == null)
|
if (dateStrings != null && dateStrings.date_begin == null) {
|
||||||
this.datesString = null;
|
this.datesString = null;
|
||||||
else this.datesString = dateStrings;
|
} else {
|
||||||
|
this.datesString = dateStrings;
|
||||||
|
}
|
||||||
|
|
||||||
if (teams != null) {
|
if (teams != null) {
|
||||||
this.teams = teams.teams;
|
this.teams = teams.teams;
|
||||||
|
@ -225,13 +228,20 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getContent(): React.Node {
|
getContent() {
|
||||||
const {state} = this;
|
const {state} = this;
|
||||||
if (!this.isVoteStarted()) return this.getTeaseVoteCard();
|
if (!this.isVoteStarted()) {
|
||||||
if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted)
|
return this.getTeaseVoteCard();
|
||||||
|
}
|
||||||
|
if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted) {
|
||||||
return this.getVoteCard();
|
return this.getVoteCard();
|
||||||
if (!this.isResultStarted()) return this.getWaitVoteCard();
|
}
|
||||||
if (this.isResultRunning()) return this.getVoteResultCard();
|
if (!this.isResultStarted()) {
|
||||||
|
return this.getWaitVoteCard();
|
||||||
|
}
|
||||||
|
if (this.isResultRunning()) {
|
||||||
|
return this.getVoteResultCard();
|
||||||
|
}
|
||||||
return <VoteNotAvailable />;
|
return <VoteNotAvailable />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,7 +250,7 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
/**
|
/**
|
||||||
* The user has not voted yet, and the votes are open
|
* The user has not voted yet, and the votes are open
|
||||||
*/
|
*/
|
||||||
getVoteCard(): React.Node {
|
getVoteCard() {
|
||||||
return (
|
return (
|
||||||
<VoteSelect
|
<VoteSelect
|
||||||
teams={this.teams}
|
teams={this.teams}
|
||||||
|
@ -253,8 +263,8 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
/**
|
/**
|
||||||
* Votes have ended, results can be displayed
|
* Votes have ended, results can be displayed
|
||||||
*/
|
*/
|
||||||
getVoteResultCard(): React.Node {
|
getVoteResultCard() {
|
||||||
if (this.dates != null && this.datesString != null)
|
if (this.dates != null && this.datesString != null) {
|
||||||
return (
|
return (
|
||||||
<VoteResults
|
<VoteResults
|
||||||
teams={this.teams}
|
teams={this.teams}
|
||||||
|
@ -264,14 +274,15 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return <VoteNotAvailable />;
|
return <VoteNotAvailable />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vote will open shortly
|
* Vote will open shortly
|
||||||
*/
|
*/
|
||||||
getTeaseVoteCard(): React.Node {
|
getTeaseVoteCard() {
|
||||||
if (this.dates != null && this.datesString != null)
|
if (this.dates != null && this.datesString != null) {
|
||||||
return (
|
return (
|
||||||
<VoteTease
|
<VoteTease
|
||||||
startDate={this.getDateString(
|
startDate={this.getDateString(
|
||||||
|
@ -280,24 +291,26 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return <VoteNotAvailable />;
|
return <VoteNotAvailable />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Votes have ended, or user has voted waiting for results
|
* Votes have ended, or user has voted waiting for results
|
||||||
*/
|
*/
|
||||||
getWaitVoteCard(): React.Node {
|
getWaitVoteCard() {
|
||||||
const {state} = this;
|
const {state} = this;
|
||||||
let startDate = null;
|
let startDate = null;
|
||||||
if (
|
if (
|
||||||
this.dates != null &&
|
this.dates != null &&
|
||||||
this.datesString != null &&
|
this.datesString != null &&
|
||||||
this.dates.date_result_begin != null
|
this.dates.date_result_begin != null
|
||||||
)
|
) {
|
||||||
startDate = this.getDateString(
|
startDate = this.getDateString(
|
||||||
this.dates.date_result_begin,
|
this.dates.date_result_begin,
|
||||||
this.datesString.date_result_begin,
|
this.datesString.date_result_begin,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<VoteWait
|
<VoteWait
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
|
@ -314,12 +327,15 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
reloadData = () => {
|
reloadData = () => {
|
||||||
let canRefresh;
|
let canRefresh;
|
||||||
const {lastRefresh} = this;
|
const {lastRefresh} = this;
|
||||||
if (lastRefresh != null)
|
if (lastRefresh != null) {
|
||||||
canRefresh =
|
canRefresh =
|
||||||
new Date().getTime() - lastRefresh.getTime() > MIN_REFRESH_TIME;
|
new Date().getTime() - lastRefresh.getTime() > MIN_REFRESH_TIME;
|
||||||
else canRefresh = true;
|
} else {
|
||||||
if (canRefresh && this.authRef.current != null)
|
canRefresh = true;
|
||||||
|
}
|
||||||
|
if (canRefresh && this.authRef.current != null) {
|
||||||
this.authRef.current.reload();
|
this.authRef.current.reload();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
showMascotDialog = () => {
|
showMascotDialog = () => {
|
||||||
|
@ -380,8 +396,12 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
date_result_begin: dateResultBegin,
|
date_result_begin: dateResultBegin,
|
||||||
date_result_end: dateResultEnd,
|
date_result_end: dateResultEnd,
|
||||||
};
|
};
|
||||||
} else this.dates = null;
|
} else {
|
||||||
} else this.dates = null;
|
this.dates = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.dates = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -391,11 +411,11 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
*
|
*
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
render(): React.Node {
|
render() {
|
||||||
const {props, state} = this;
|
const {props, state} = this;
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1}}>
|
<View style={{flex: 1}}>
|
||||||
<AuthenticatedScreen
|
<AuthenticatedScreen<TeamResponseType | VoteDatesStringType>
|
||||||
navigation={props.navigation}
|
navigation={props.navigation}
|
||||||
ref={this.authRef}
|
ref={this.authRef}
|
||||||
requests={[
|
requests={[
|
||||||
|
@ -418,7 +438,6 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
message={i18n.t('screens.vote.mascotDialog.message')}
|
message={i18n.t('screens.vote.mascotDialog.message')}
|
||||||
icon="vote"
|
icon="vote"
|
||||||
buttons={{
|
buttons={{
|
||||||
action: null,
|
|
||||||
cancel: {
|
cancel: {
|
||||||
message: i18n.t('screens.vote.mascotDialog.button'),
|
message: i18n.t('screens.vote.mascotDialog.button'),
|
||||||
icon: 'check',
|
icon: 'check',
|
|
@ -35,8 +35,6 @@ export type ApiDataLoginType = {
|
||||||
token: string;
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApiGenericDataType = {[key: string]: any};
|
|
||||||
|
|
||||||
type ApiResponseType<T> = {
|
type ApiResponseType<T> = {
|
||||||
error: number;
|
error: number;
|
||||||
data: T;
|
data: T;
|
||||||
|
@ -70,7 +68,7 @@ export function isApiResponseValid<T>(response: ApiResponseType<T>): boolean {
|
||||||
* @param path The API path from the API endpoint
|
* @param path The API path from the API endpoint
|
||||||
* @param method The HTTP method to use (GET or POST)
|
* @param method The HTTP method to use (GET or POST)
|
||||||
* @param params The params to use for this request
|
* @param params The params to use for this request
|
||||||
* @returns {Promise<ApiGenericDataType>}
|
* @returns {Promise<T>}
|
||||||
*/
|
*/
|
||||||
export async function apiRequest<T>(
|
export async function apiRequest<T>(
|
||||||
path: string,
|
path: string,
|
||||||
|
|
Loading…
Reference in a new issue