Merge pull request #3 from zdam-egzamin-zawodowy/QuestionsPage

add QuestionsPage
This commit is contained in:
Dawid Wysokiński 2021-03-14 11:40:55 +01:00 committed by GitHub
commit cee5dbcb72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 989 additions and 138 deletions

View File

@ -9,3 +9,4 @@ generates:
skipTypename: true
scalars:
ID: number
Time: Date | string

View File

@ -16,6 +16,7 @@ import {
Group as GroupIcon,
Work as WorkIcon,
Description as DescriptionIcon,
QuestionAnswer as QuestionAnswerIcon,
} from '@material-ui/icons';
import Nav from './components/Nav/Nav';
import CurrentUser from './components/CurrentUser/CurrentUser';
@ -51,6 +52,12 @@ const Sidebar = ({ className, open, variant, onClose, onOpen }: Props) => {
exact: true,
Icon: <DescriptionIcon color="inherit" />,
},
{
name: 'Pytania',
to: Route.QuestionsPage,
exact: true,
Icon: <QuestionAnswerIcon color="inherit" />,
},
];
useEffect(() => {

2
src/config/cdn.ts Normal file
View File

@ -0,0 +1,2 @@
export const CDN_URI =
process.env.REACT_APP_CDN_URI ?? 'http://localhost:9000/';

View File

@ -3,6 +3,7 @@ export enum Route {
UsersPage = '/users',
ProfessionsPage = '/professions',
QualificationsPage = '/qualifications',
QuestionsPage = '/questions',
}
export const PUBLIC_ROUTES = [Route.SignInPage];

View File

@ -4,6 +4,7 @@ import AppLayout from 'common/AppLayout/AppLayout';
import UsersPage from './UsersPage/UsersPage';
import ProfessionsPage from './ProfessionsPage/ProfessionsPage';
import QualificationsPage from './QualificationsPage/QualificationsPage';
import QuestionsPage from './QuestionsPage/QuestionsPage';
function AdminRoutes() {
return (
@ -18,6 +19,9 @@ function AdminRoutes() {
<RRDRoute exact path={Route.QualificationsPage}>
<QualificationsPage />
</RRDRoute>
<RRDRoute exact path={Route.QuestionsPage}>
<QuestionsPage />
</RRDRoute>
</Switch>
</AppLayout>
);

View File

@ -0,0 +1,224 @@
import { useState } from 'react';
import { ApolloError, useMutation } from '@apollo/client';
import { useUpdateEffect } from 'react-use';
import {
NumberParam,
StringParam,
useQueryParams,
withDefault,
} from 'use-query-params';
import { useSnackbar } from 'notistack';
import SortParam, { decodeSort } from 'libs/serialize-query-params/SortParam';
import useQuestions from './QuestionsPage.useQuestions';
import { validateRowsPerPage } from 'common/Table/helpers';
import {
MUTATION_CREATE_QUESTION,
MUTATION_UPDATE_QUESTION,
MUTATION_DELETE_QUESTIONS,
} from './mutations';
import { COLUMNS, DEFAULT_SORT, DialogType } from './constants';
import {
Maybe,
MutationCreateQuestionArgs,
MutationDeleteQuestionsArgs,
MutationUpdateQuestionArgs,
Question,
QuestionInput,
} from 'libs/graphql/types';
import {
Button,
Container,
IconButton,
Paper,
Snackbar,
} from '@material-ui/core';
import { Edit as EditIcon } from '@material-ui/icons';
import Table from 'common/Table/Table';
import TableToolbar from './components/TableToolbar/TableToolbar';
import FormDialog from './components/FormDialog/FormDialog';
const QuestionsPage = () => {
const [createQuestionMutation] = useMutation<any, MutationCreateQuestionArgs>(
MUTATION_CREATE_QUESTION,
{ ignoreResults: true }
);
const [updateQuestionMutation] = useMutation<any, MutationUpdateQuestionArgs>(
MUTATION_UPDATE_QUESTION,
{ ignoreResults: true }
);
const [deleteQuestionsMutation] = useMutation<
any,
MutationDeleteQuestionsArgs
>(MUTATION_DELETE_QUESTIONS, { ignoreResults: true });
const [dialogType, setDialogType] = useState<DialogType>(DialogType.None);
const [questionBeingEdited, setQuestionBeingEdited] = useState<
Maybe<Question>
>(null);
const [selectedQuestions, setSelectedQuestions] = useState<Question[]>([]);
const snackbar = useSnackbar();
const [{ page, sort, search, ...rest }, setQueryParams] = useQueryParams({
limit: NumberParam,
page: withDefault(NumberParam, 0),
sort: withDefault(SortParam, DEFAULT_SORT),
search: withDefault(StringParam, ''),
});
const limit = validateRowsPerPage(rest.limit);
const { questions, total, loading, refetch } = useQuestions(
page,
limit,
sort.toString(),
search
);
useUpdateEffect(() => {
if (selectedQuestions.length > 0) {
setSelectedQuestions([]);
}
}, [questions]);
const handleFormDialogSubmit = async (input: QuestionInput) => {
try {
if (dialogType === DialogType.Create) {
await createQuestionMutation({ variables: { input } });
} else {
await updateQuestionMutation({
variables: { input, id: questionBeingEdited?.id ?? -1 },
});
}
await refetch();
snackbar.enqueueSnackbar(
dialogType === DialogType.Create
? 'Pomyślnie utworzono pytanie.'
: 'Zapisano zmiany.',
{ variant: 'success' }
);
return true;
} catch (e) {
snackbar.enqueueSnackbar(
e instanceof ApolloError && e.graphQLErrors.length > 0
? e.graphQLErrors[0].message
: e.message,
{ variant: 'error' }
);
}
return false;
};
const handleDeleteQuestions = async () => {
if (!window.confirm('Czy na pewno chcesz usunąć wybrane pytania?')) {
return;
}
try {
const ids = selectedQuestions.map(question => question.id);
await deleteQuestionsMutation({ variables: { ids } });
await refetch();
snackbar.enqueueSnackbar(`Usuwanie przebiegło pomyślnie.`, {
variant: 'success',
});
} catch (e) {
snackbar.enqueueSnackbar(
e instanceof ApolloError && e.graphQLErrors.length > 0
? e.graphQLErrors[0].message
: e.message,
{ variant: 'error' }
);
}
};
const handleSelect = (checked: boolean, items: Question[]) => {
setSelectedQuestions(prevState =>
checked
? [...prevState, ...items]
: prevState.filter(
item => !items.some(otherItem => otherItem.id === item.id)
)
);
};
return (
<Container>
<Paper>
<TableToolbar
search={search}
onClickCreateQuestion={() => {
setQuestionBeingEdited(null);
setDialogType(DialogType.Create);
}}
onChangeSearchValue={val => {
setQueryParams({ page: 0, search: val });
}}
/>
<Table
selection
columns={COLUMNS}
data={questions}
selected={selectedQuestions}
onSelect={handleSelect}
actions={[
{
icon: row => {
return (
<IconButton
onClick={() => {
setQuestionBeingEdited(row);
setDialogType(DialogType.Edit);
}}
>
<EditIcon />
</IconButton>
);
},
tooltip: 'Edytuj',
},
]}
loading={loading}
orderBy={sort.orderBy}
orderDirection={sort.orderDirection}
onRequestSort={(orderBy, orderDirection) => {
setQueryParams({
page: 0,
sort: decodeSort(orderBy + ' ' + orderDirection),
});
}}
footerProps={{
count: total,
page,
onChangePage: page => {
setQueryParams({ page });
},
onChangeRowsPerPage: limit => {
setQueryParams({ page: 0, limit });
},
rowsPerPage: limit,
}}
/>
</Paper>
<FormDialog
open={
dialogType === DialogType.Create || dialogType === DialogType.Edit
}
question={questionBeingEdited}
onSubmit={handleFormDialogSubmit}
onClose={() => setDialogType(DialogType.None)}
/>
<Snackbar
open={selectedQuestions.length > 0}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
message={`Wybrane pytania: ${selectedQuestions.length}`}
action={
<>
<Button onClick={handleDeleteQuestions} color="secondary">
Usuń
</Button>
<Button color="secondary" onClick={() => setSelectedQuestions([])}>
Anuluj
</Button>
</>
}
/>
</Container>
);
};
export default QuestionsPage;

View File

@ -0,0 +1,36 @@
import { useQuery } from '@apollo/client';
import { QUERY_QUESTIONS } from './queries';
import { Query, QueryQuestionsArgs } from 'libs/graphql/types';
const useQuestions = (
page: number,
limit: number,
sort: string,
search: string
) => {
const { data, loading, refetch } = useQuery<
Pick<Query, 'questions'>,
QueryQuestionsArgs
>(QUERY_QUESTIONS, {
fetchPolicy: 'cache-and-network',
variables: {
offset: page * limit,
sort: [sort],
limit,
filter: {
contentIEQ: '%' + search + '%',
},
},
});
return {
questions: data?.questions.items ?? [],
get loading() {
return this.questions.length === 0 && loading;
},
total: data?.questions.total ?? 0,
refetch,
};
};
export default useQuestions;

View File

@ -0,0 +1,294 @@
import { useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { get } from 'lodash';
import useQualifications from './FormDialog.useQualifications';
import { capitalizeFirstLetter } from './helpers';
import { QuestionInput, Question, Maybe, Answer } from 'libs/graphql/types';
import { makeStyles } from '@material-ui/core/styles';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogProps,
DialogTitle,
MenuItem,
TextField,
TextFieldProps,
} from '@material-ui/core';
import ImagePreview from './ImagePreview';
const ANSWERS = Object.values(Answer);
export interface FormDialogProps extends Pick<DialogProps, 'open'> {
question?: Maybe<Question>;
onClose: () => void;
onSubmit: (input: QuestionInput) => Promise<boolean> | boolean;
}
type Images = Pick<
QuestionInput,
| 'deleteImage'
| 'deleteAnswerAImage'
| 'deleteAnswerBImage'
| 'deleteAnswerCImage'
| 'deleteAnswerDImage'
> & {
image?: FileList;
answerAImage?: FileList;
answerBImage?: FileList;
answerCImage?: FileList;
answerDImage?: FileList;
};
const FormDialog = ({ open, onClose, question, onSubmit }: FormDialogProps) => {
const editMode = Boolean(question);
const {
register,
handleSubmit,
errors,
control,
formState: { isSubmitting },
watch,
setValue,
reset,
} = useForm<QuestionInput>({});
const images: Images = watch([
'image',
'deleteImage',
'answerAImage',
'deleteAnswerAImage',
'answerBImage',
'deleteAnswerBImage',
'answerCImage',
'deleteAnswerCImage',
'answerDImage',
'deleteAnswerDImage',
]);
const {
qualifications,
loading: loadingQualifications,
} = useQualifications();
const classes = useStyles();
useEffect(() => {
[
'deleteImage',
'deleteAnswerAImage',
'deleteAnswerBImage',
'deleteAnswerCImage',
'deleteAnswerDImage',
].forEach(key => {
register(key);
});
}, [register]);
const _onSubmit = async (data: QuestionInput) => {
const success = await onSubmit({
...data,
image: data.image?.item(0),
answerAImage: data.answerAImage?.item(0),
answerBImage: data.answerBImage?.item(0),
answerCImage: data.answerCImage?.item(0),
answerDImage: data.answerDImage?.item(0),
});
if (success) {
onClose();
reset();
}
};
const renderImagePreview = (key: keyof Images) => {
const deleteKey = ('delete' + capitalizeFirstLetter(key)) as keyof Images;
const uploadedImage = images[key] as FileList | undefined;
if (
(uploadedImage && uploadedImage.length > 0) ||
(question && get(question, key) && !get(images, deleteKey))
) {
let src = get(question, key, '');
if (src && question) {
src += `?` + new Date(question.updatedAt).getTime();
}
return (
<ImagePreview
file={uploadedImage?.item(0)}
src={src}
onDelete={() => {
setValue(key, undefined);
setValue(deleteKey, true);
}}
/>
);
}
return null;
};
const defaultFileInputProps: TextFieldProps = {
type: 'file',
InputLabelProps: {
shrink: true,
},
inputProps: {
accept: ['image/*'],
multiple: false,
},
};
return (
<Dialog
open={open}
onClose={isSubmitting ? undefined : onClose}
fullWidth
maxWidth="xs"
>
<form onSubmit={handleSubmit(_onSubmit)}>
<DialogTitle>
{editMode ? 'Edycja pytania' : 'Tworzenie pytania'}
</DialogTitle>
<DialogContent className={classes.dialogContent}>
{renderImagePreview('image')}
<TextField
fullWidth
label="Obrazek"
name="image"
{...defaultFileInputProps}
inputRef={register}
error={!!errors.image}
helperText={errors.image?.message ?? ''}
/>
<TextField
fullWidth
label="Treść pytania"
name="content"
multiline
defaultValue={question?.content}
inputRef={register({
required: 'Te pole jest wymagane.',
})}
error={!!errors.content}
helperText={errors.content?.message ?? ''}
/>
<TextField
fullWidth
label="Z"
name="from"
defaultValue={question?.from}
inputRef={register({
required: 'Te pole jest wymagane.',
})}
error={!!errors.from}
helperText={errors.from?.message ?? ''}
/>
<TextField
fullWidth
label="Wyjaśnienie"
name="explanation"
multiline
defaultValue={question?.explanation}
inputRef={register}
error={!!errors.explanation}
helperText={errors.explanation?.message ?? ''}
/>
<Controller
name="correctAnswer"
defaultValue={question?.correctAnswer ?? ANSWERS[0]}
control={control}
as={
<TextField select fullWidth label="Poprawna odpowiedź">
{ANSWERS.map(answer => (
<MenuItem key={answer} value={answer}>
{answer}
</MenuItem>
))}
</TextField>
}
/>
{ANSWERS.map(answer => {
const upper = answer.toUpperCase();
const key = `answer${upper}` as keyof QuestionInput;
const imageKey = `${key}Image` as keyof Images;
return (
<div key={upper} className={classes.dialogContent}>
{renderImagePreview(imageKey)}
<TextField
fullWidth
label={`Odpowiedź ${upper} obrazek`}
name={imageKey}
{...defaultFileInputProps}
inputRef={register}
error={!!errors[imageKey]}
helperText={errors[imageKey]?.message ?? ''}
/>
<TextField
fullWidth
label={`Odpowiedź ${upper}`}
name={key}
multiline
defaultValue={question ? question[key as keyof Question] : ''}
inputRef={register({
required: 'Te pole jest wymagane.',
})}
error={!!errors[key]}
helperText={errors[key]?.message ?? ''}
/>
</div>
);
})}
{!loadingQualifications && (
<Controller
name="qualificationID"
defaultValue={
question?.qualification?.id ?? qualifications[0]?.id
}
control={control}
rules={{
valueAsNumber: true,
required: 'Musisz wybrać kwalifikację.',
}}
as={
<TextField select fullWidth label="Kwalifikacja">
{qualifications.map(qualification => (
<MenuItem key={qualification.id} value={qualification.id}>
{qualification.name} ({qualification.code})
</MenuItem>
))}
</TextField>
}
/>
)}
</DialogContent>
<DialogActions>
<Button
color="secondary"
type="button"
variant="contained"
onClick={onClose}
disabled={isSubmitting}
>
Anuluj
</Button>
<Button
type="submit"
color="primary"
variant="contained"
disabled={isSubmitting}
>
{editMode ? 'Zapisz' : 'Utwórz'}
</Button>
</DialogActions>
</form>
</Dialog>
);
};
const useStyles = makeStyles(theme => ({
dialogContent: {
'& > *:not(:last-child)': {
marginBottom: theme.spacing(1),
},
},
}));
export default FormDialog;

View File

@ -0,0 +1,26 @@
import { useQuery } from '@apollo/client';
import { QUERY_QUALIFICATIONS } from './queries';
import { Query, QueryQualificationsArgs } from 'libs/graphql/types';
const useQualifications = () => {
const { data, loading, refetch } = useQuery<
Pick<Query, 'qualifications'>,
QueryQualificationsArgs
>(QUERY_QUALIFICATIONS, {
fetchPolicy: 'cache-and-network',
variables: {
sort: ['id ASC'],
},
});
return {
qualifications: data?.qualifications.items ?? [],
get loading() {
return this.qualifications.length === 0 && loading;
},
total: data?.qualifications.total ?? 0,
refetch,
};
};
export default useQualifications;

View File

@ -0,0 +1,55 @@
import { useEffect, useState } from 'react';
import buildURL from 'utils/buildURL';
import { Tooltip, IconButton, Box } from '@material-ui/core';
import { Delete as DeleteIcon } from '@material-ui/icons';
export interface PreviewImageProps {
file?: File | null;
src?: string | null;
onDelete?: () => void;
disabled?: boolean;
}
const PreviewImage = ({ file, src, onDelete, disabled }: PreviewImageProps) => {
const [_src, _setSrc] = useState<string>('');
useEffect(() => {
if (!file && !src && process.env.NODE_ENV === 'development') {
console.warn('PreviewImage: you should specify file or src');
}
if (!file) return _setSrc(buildURL('cdn', src ?? ''));
const reader = new FileReader();
reader.onload = function (e) {
_setSrc(typeof e.target?.result === 'string' ? e.target.result : '');
};
reader.readAsDataURL(file);
}, [src, file]);
return (
<Box textAlign="center">
<Box position="relative" display="inline-block">
<Box position="absolute" right="2%" top="2%">
<Tooltip title="Usuń">
<IconButton
disabled={disabled}
color={'secondary'}
onClick={onDelete}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</Box>
<img
src={_src}
alt="Podgląd zdjęcia"
style={{ maxWidth: '100%', maxHeight: '400px' }}
/>
</Box>
</Box>
);
};
export default PreviewImage;

View File

@ -0,0 +1,2 @@
export const capitalizeFirstLetter = (str: string) =>
str[0].toUpperCase() + str.slice(1);

View File

@ -0,0 +1,24 @@
import { gql } from '@apollo/client';
export const QUERY_QUALIFICATIONS = gql`
query qualifications(
$offset: Int
$limit: Int
$filter: QualificationFilter
$sort: [String!]
) {
qualifications(
offset: $offset
limit: $limit
filter: $filter
sort: $sort
) {
total
items {
id
name
code
}
}
}
`;

View File

@ -0,0 +1,60 @@
import { useState } from 'react';
import { useDebounce } from 'react-use';
import { makeStyles } from '@material-ui/core/styles';
import { IconButton, Tooltip } from '@material-ui/core';
import { Add as AddIcon } from '@material-ui/icons';
import BaseTableToolbar from 'common/Table/TableToolbar';
import SearchInput from 'common/Form/SearchInput';
export interface TableToolbarProps {
search: string;
onChangeSearchValue: (search: string) => void;
onClickCreateQuestion: () => void;
}
const TableToolbar = ({
search,
onChangeSearchValue,
onClickCreateQuestion,
}: TableToolbarProps) => {
const classes = useStyles();
const [_search, setSearch] = useState<string>(search);
useDebounce(
() => {
onChangeSearchValue(_search);
},
500,
[_search]
);
return (
<BaseTableToolbar className={classes.toolbar}>
<SearchInput
value={_search}
onChange={e => {
setSearch(e.target.value);
}}
onResetValue={() => {
setSearch('');
}}
/>
<Tooltip title="Utwórz pytanie">
<IconButton onClick={onClickCreateQuestion}>
<AddIcon />
</IconButton>
</Tooltip>
</BaseTableToolbar>
);
};
const useStyles = makeStyles(theme => ({
toolbar: {
justifyContent: 'flex-end',
'& > *:not(:last-child)': {
marginRight: theme.spacing(0.5),
},
},
}));
export default TableToolbar;

View File

@ -0,0 +1,43 @@
import { decodeSort } from 'libs/serialize-query-params/SortParam';
import { Column } from 'common/Table/types';
import { Question } from 'libs/graphql/types';
export const DEFAULT_SORT = decodeSort('id DESC');
export const COLUMNS: Column<Question>[] = [
{
field: 'id',
sortable: true,
label: 'ID',
},
{
field: 'from',
sortable: true,
label: 'Z',
},
{
field: 'content',
sortable: true,
label: 'Treść',
},
{
field: 'qualification',
sortable: false,
label: 'Kwalifikacja',
valueFormatter: v => {
return `${v.qualification?.code ?? '-'} (ID: ${
v.qualification?.id ?? 0
})`;
},
},
{
field: 'createdAt',
sortable: true,
label: 'Data utworzenia',
type: 'datetime',
},
];
export enum DialogType {
Create = 'create',
Edit = 'edit',
None = '',
}

View File

@ -0,0 +1,25 @@
import { gql } from '@apollo/client';
export const MUTATION_CREATE_QUESTION = gql`
mutation createQuestion($input: QuestionInput!) {
createQuestion(input: $input) {
id
}
}
`;
export const MUTATION_UPDATE_QUESTION = gql`
mutation updateQuestion($id: ID!, $input: QuestionInput!) {
updateQuestion(id: $id, input: $input) {
id
}
}
`;
export const MUTATION_DELETE_QUESTIONS = gql`
mutation deleteQuestions($ids: [ID!]!) {
deleteQuestions(ids: $ids) {
id
}
}
`;

View File

@ -0,0 +1,36 @@
import { gql } from '@apollo/client';
export const QUERY_QUESTIONS = gql`
query questions(
$offset: Int
$limit: Int
$filter: QuestionFilter
$sort: [String!]
) {
questions(offset: $offset, limit: $limit, filter: $filter, sort: $sort) {
total
items {
id
content
image
from
answerA
answerAImage
answerB
answerBImage
answerC
answerCImage
answerD
answerDImage
explanation
correctAnswer
qualification {
id
code
}
createdAt
updatedAt
}
}
}
`;

View File

@ -9,33 +9,48 @@ export type Scalars = {
Boolean: boolean;
Int: number;
Float: number;
Time: any;
Upload: any;
Time: Date | string;
};
export type UserWithToken = {
token: Scalars['String'];
user: User;
};
export type ProfessionInput = {
name?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
};
export type QualificationList = {
export type UserList = {
total: Scalars['Int'];
items?: Maybe<Array<Qualification>>;
items?: Maybe<Array<User>>;
};
export type UserInput = {
displayName?: Maybe<Scalars['String']>;
password?: Maybe<Scalars['String']>;
email?: Maybe<Scalars['String']>;
role?: Maybe<Role>;
activated?: Maybe<Scalars['Boolean']>;
};
export type QuestionFilter = {
id?: Maybe<Array<Scalars['ID']>>;
idNEQ?: Maybe<Array<Scalars['ID']>>;
from?: Maybe<Array<Scalars['String']>>;
contentIEQ?: Maybe<Scalars['String']>;
contentMATCH?: Maybe<Scalars['String']>;
qualificationID?: Maybe<Array<Scalars['Int']>>;
qualificationIDNEQ?: Maybe<Array<Scalars['Int']>>;
qualificationFilter?: Maybe<QualificationFilter>;
createdAt?: Maybe<Scalars['Time']>;
createdAtGT?: Maybe<Scalars['Time']>;
createdAtGTE?: Maybe<Scalars['Time']>;
createdAtLT?: Maybe<Scalars['Time']>;
createdAtLTE?: Maybe<Scalars['Time']>;
};
export enum Role {
Admin = 'admin',
User = 'user'
}
export type User = {
id: Scalars['ID'];
@ -53,6 +68,25 @@ export type UserFilterOr = {
emailMATCH?: Maybe<Scalars['String']>;
};
export type ProfessionFilter = {
id?: Maybe<Array<Scalars['ID']>>;
idNEQ?: Maybe<Array<Scalars['ID']>>;
slug?: Maybe<Array<Scalars['String']>>;
slugNEQ?: Maybe<Array<Scalars['String']>>;
name?: Maybe<Array<Scalars['String']>>;
nameNEQ?: Maybe<Array<Scalars['String']>>;
nameIEQ?: Maybe<Scalars['String']>;
nameMATCH?: Maybe<Scalars['String']>;
descriptionIEQ?: Maybe<Scalars['String']>;
descriptionMATCH?: Maybe<Scalars['String']>;
qualificationID?: Maybe<Array<Scalars['ID']>>;
createdAt?: Maybe<Scalars['Time']>;
createdAtGT?: Maybe<Scalars['Time']>;
createdAtGTE?: Maybe<Scalars['Time']>;
createdAtLT?: Maybe<Scalars['Time']>;
createdAtLTE?: Maybe<Scalars['Time']>;
};
export type Qualification = {
id: Scalars['ID'];
slug: Scalars['String'];
@ -63,12 +97,17 @@ export type Qualification = {
createdAt: Scalars['Time'];
};
export enum Answer {
A = 'a',
B = 'b',
C = 'c',
D = 'd'
}
export type QualificationFilterOr = {
nameMatch?: Maybe<Scalars['String']>;
nameIEQ?: Maybe<Scalars['String']>;
codeMatch?: Maybe<Scalars['String']>;
codeIEQ?: Maybe<Scalars['String']>;
};
export type QuestionList = {
total: Scalars['Int'];
items?: Maybe<Array<Question>>;
};
export type QuestionInput = {
content?: Maybe<Scalars['String']>;
@ -78,78 +117,45 @@ export type QuestionInput = {
qualificationID?: Maybe<Scalars['Int']>;
image?: Maybe<Scalars['Upload']>;
deleteImage?: Maybe<Scalars['Boolean']>;
answerA?: Maybe<Answer>;
answerA?: Maybe<Scalars['String']>;
answerAImage?: Maybe<Scalars['Upload']>;
deleteAnswerAImage?: Maybe<Scalars['Boolean']>;
answerB?: Maybe<Answer>;
answerB?: Maybe<Scalars['String']>;
answerBImage?: Maybe<Scalars['Upload']>;
deleteAnswerBImage?: Maybe<Scalars['Boolean']>;
answerC?: Maybe<Answer>;
answerC?: Maybe<Scalars['String']>;
answerCImage?: Maybe<Scalars['Upload']>;
deleteAnswerCImage?: Maybe<Scalars['Boolean']>;
answerD?: Maybe<Answer>;
answerD?: Maybe<Scalars['String']>;
answerDImage?: Maybe<Scalars['Upload']>;
deleteAnswerDImage?: Maybe<Scalars['Boolean']>;
};
export type Question = {
id: Scalars['ID'];
from?: Maybe<Scalars['String']>;
content: Scalars['String'];
explanation?: Maybe<Scalars['String']>;
correctAnswer: Answer;
image?: Maybe<Scalars['String']>;
answerA?: Maybe<Answer>;
answerAImage?: Maybe<Scalars['String']>;
answerB?: Maybe<Answer>;
answerBImage?: Maybe<Scalars['String']>;
answerC?: Maybe<Answer>;
answerCImage?: Maybe<Scalars['String']>;
answerD?: Maybe<Answer>;
answerDImage?: Maybe<Scalars['String']>;
qualification?: Maybe<Qualification>;
createdAt: Scalars['Time'];
updatedAt: Scalars['Time'];
export type UpdateManyUsersInput = {
role?: Maybe<Role>;
activated?: Maybe<Scalars['Boolean']>;
};
export type ProfessionList = {
total: Scalars['Int'];
items?: Maybe<Array<Profession>>;
};
export type QualificationInput = {
name?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
code?: Maybe<Scalars['String']>;
formula?: Maybe<Scalars['String']>;
associateProfession?: Maybe<Array<Scalars['Int']>>;
dissociateProfession?: Maybe<Array<Scalars['Int']>>;
};
export type QualificationFilter = {
export type UserFilter = {
id?: Maybe<Array<Scalars['ID']>>;
idNEQ?: Maybe<Array<Scalars['ID']>>;
slug?: Maybe<Array<Scalars['String']>>;
slugNEQ?: Maybe<Array<Scalars['String']>>;
formula?: Maybe<Array<Scalars['String']>>;
formulaNEQ?: Maybe<Array<Scalars['String']>>;
name?: Maybe<Array<Scalars['String']>>;
nameNEQ?: Maybe<Array<Scalars['String']>>;
nameIEQ?: Maybe<Scalars['String']>;
nameMATCH?: Maybe<Scalars['String']>;
code?: Maybe<Array<Scalars['String']>>;
codeNEQ?: Maybe<Array<Scalars['String']>>;
codeIEQ?: Maybe<Scalars['String']>;
codeMATCH?: Maybe<Scalars['String']>;
descriptionIEQ?: Maybe<Scalars['String']>;
descriptionMATCH?: Maybe<Scalars['String']>;
professionID?: Maybe<Array<Scalars['Int']>>;
activated?: Maybe<Scalars['Boolean']>;
displayName?: Maybe<Array<Scalars['String']>>;
displayNameNEQ?: Maybe<Array<Scalars['String']>>;
displayNameIEQ?: Maybe<Scalars['String']>;
displayNameMATCH?: Maybe<Scalars['String']>;
email?: Maybe<Array<Scalars['String']>>;
emailNEQ?: Maybe<Array<Scalars['String']>>;
emailIEQ?: Maybe<Scalars['String']>;
emailMATCH?: Maybe<Scalars['String']>;
role?: Maybe<Array<Role>>;
roleNEQ?: Maybe<Array<Role>>;
createdAt?: Maybe<Scalars['Time']>;
createdAtGT?: Maybe<Scalars['Time']>;
createdAtGTE?: Maybe<Scalars['Time']>;
createdAtLT?: Maybe<Scalars['Time']>;
createdAtLTE?: Maybe<Scalars['Time']>;
or?: Maybe<QualificationFilterOr>;
or?: Maybe<UserFilterOr>;
};
export type Mutation = {
@ -246,52 +252,17 @@ export type MutationSignInArgs = {
staySignedIn?: Maybe<Scalars['Boolean']>;
};
export type UserFilter = {
id?: Maybe<Array<Scalars['ID']>>;
idNEQ?: Maybe<Array<Scalars['ID']>>;
activated?: Maybe<Scalars['Boolean']>;
displayName?: Maybe<Array<Scalars['String']>>;
displayNameNEQ?: Maybe<Array<Scalars['String']>>;
displayNameIEQ?: Maybe<Scalars['String']>;
displayNameMATCH?: Maybe<Scalars['String']>;
email?: Maybe<Array<Scalars['String']>>;
emailNEQ?: Maybe<Array<Scalars['String']>>;
emailIEQ?: Maybe<Scalars['String']>;
emailMATCH?: Maybe<Scalars['String']>;
role?: Maybe<Array<Role>>;
roleNEQ?: Maybe<Array<Role>>;
createdAt?: Maybe<Scalars['Time']>;
createdAtGT?: Maybe<Scalars['Time']>;
createdAtGTE?: Maybe<Scalars['Time']>;
createdAtLT?: Maybe<Scalars['Time']>;
createdAtLTE?: Maybe<Scalars['Time']>;
or?: Maybe<UserFilterOr>;
};
export enum Role {
Admin = 'admin',
User = 'user'
}
export type Profession = {
id: Scalars['ID'];
slug: Scalars['String'];
name: Scalars['String'];
description?: Maybe<Scalars['String']>;
createdAt: Scalars['Time'];
};
export type QuestionFilter = {
id?: Maybe<Array<Scalars['ID']>>;
idNEQ?: Maybe<Array<Scalars['ID']>>;
from?: Maybe<Array<Scalars['String']>>;
contentIEQ?: Maybe<Scalars['String']>;
contentMATCH?: Maybe<Scalars['String']>;
qualificationID?: Maybe<Array<Scalars['Int']>>;
qualificationIDNEQ?: Maybe<Array<Scalars['Int']>>;
qualificationFilter?: Maybe<QualificationFilter>;
createdAt?: Maybe<Scalars['Time']>;
createdAtGT?: Maybe<Scalars['Time']>;
createdAtGTE?: Maybe<Scalars['Time']>;
createdAtLT?: Maybe<Scalars['Time']>;
createdAtLTE?: Maybe<Scalars['Time']>;
};
export enum Answer {
A = 'a',
B = 'b',
C = 'c',
D = 'd'
}
export type Query = {
professions: ProfessionList;
@ -360,51 +331,80 @@ export type QueryUserArgs = {
id: Scalars['Int'];
};
export type ProfessionFilter = {
export type Profession = {
id: Scalars['ID'];
slug: Scalars['String'];
name: Scalars['String'];
description?: Maybe<Scalars['String']>;
createdAt: Scalars['Time'];
};
export type QualificationInput = {
name?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
code?: Maybe<Scalars['String']>;
formula?: Maybe<Scalars['String']>;
associateProfession?: Maybe<Array<Scalars['Int']>>;
dissociateProfession?: Maybe<Array<Scalars['Int']>>;
};
export type QualificationFilter = {
id?: Maybe<Array<Scalars['ID']>>;
idNEQ?: Maybe<Array<Scalars['ID']>>;
slug?: Maybe<Array<Scalars['String']>>;
slugNEQ?: Maybe<Array<Scalars['String']>>;
formula?: Maybe<Array<Scalars['String']>>;
formulaNEQ?: Maybe<Array<Scalars['String']>>;
name?: Maybe<Array<Scalars['String']>>;
nameNEQ?: Maybe<Array<Scalars['String']>>;
nameIEQ?: Maybe<Scalars['String']>;
nameMATCH?: Maybe<Scalars['String']>;
code?: Maybe<Array<Scalars['String']>>;
codeNEQ?: Maybe<Array<Scalars['String']>>;
codeIEQ?: Maybe<Scalars['String']>;
codeMATCH?: Maybe<Scalars['String']>;
descriptionIEQ?: Maybe<Scalars['String']>;
descriptionMATCH?: Maybe<Scalars['String']>;
qualificationID?: Maybe<Array<Scalars['ID']>>;
professionID?: Maybe<Array<Scalars['Int']>>;
createdAt?: Maybe<Scalars['Time']>;
createdAtGT?: Maybe<Scalars['Time']>;
createdAtGTE?: Maybe<Scalars['Time']>;
createdAtLT?: Maybe<Scalars['Time']>;
createdAtLTE?: Maybe<Scalars['Time']>;
or?: Maybe<QualificationFilterOr>;
};
export type QualificationFilterOr = {
nameMatch?: Maybe<Scalars['String']>;
nameIEQ?: Maybe<Scalars['String']>;
codeMatch?: Maybe<Scalars['String']>;
codeIEQ?: Maybe<Scalars['String']>;
export type UserWithToken = {
token: Scalars['String'];
user: User;
};
export type UserInput = {
displayName?: Maybe<Scalars['String']>;
password?: Maybe<Scalars['String']>;
email?: Maybe<Scalars['String']>;
role?: Maybe<Role>;
activated?: Maybe<Scalars['Boolean']>;
};
export type QuestionList = {
export type ProfessionList = {
total: Scalars['Int'];
items?: Maybe<Array<Question>>;
items?: Maybe<Array<Profession>>;
};
export type UserList = {
export type QualificationList = {
total: Scalars['Int'];
items?: Maybe<Array<User>>;
items?: Maybe<Array<Qualification>>;
};
export type UpdateManyUsersInput = {
role?: Maybe<Role>;
activated?: Maybe<Scalars['Boolean']>;
export type Question = {
id: Scalars['ID'];
from?: Maybe<Scalars['String']>;
content: Scalars['String'];
explanation?: Maybe<Scalars['String']>;
correctAnswer: Answer;
image?: Maybe<Scalars['String']>;
answerA?: Maybe<Scalars['String']>;
answerAImage?: Maybe<Scalars['String']>;
answerB?: Maybe<Scalars['String']>;
answerBImage?: Maybe<Scalars['String']>;
answerC?: Maybe<Scalars['String']>;
answerCImage?: Maybe<Scalars['String']>;
answerD?: Maybe<Scalars['String']>;
answerDImage?: Maybe<Scalars['String']>;
qualification?: Maybe<Qualification>;
createdAt: Scalars['Time'];
updatedAt: Scalars['Time'];
};

11
src/utils/buildURL.ts Normal file
View File

@ -0,0 +1,11 @@
import { CDN_URI } from 'config/cdn';
const buildURL = (type: 'cdn', str: string): string => {
switch (type) {
case 'cdn':
return CDN_URI + str;
}
return str;
};
export default buildURL;