move move frontend to progress-test
This commit is contained in:
4716
app/javascript/__generated__/graphql-schema.json
generated
Normal file
4716
app/javascript/__generated__/graphql-schema.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
541
app/javascript/__generated__/graphql-schema.ts
generated
Normal file
541
app/javascript/__generated__/graphql-schema.ts
generated
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
export type Maybe<T> = T | null;
|
||||||
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
|
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
||||||
|
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
||||||
|
/** All built-in and custom scalars, mapped to their actual values */
|
||||||
|
export type Scalars = {
|
||||||
|
ID: string;
|
||||||
|
String: string;
|
||||||
|
Boolean: boolean;
|
||||||
|
Int: number;
|
||||||
|
Float: number;
|
||||||
|
/** An ISO 8601-encoded date */
|
||||||
|
ISO8601Date: any;
|
||||||
|
/** An ISO 8601-encoded datetime */
|
||||||
|
ISO8601DateTime: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Axis = {
|
||||||
|
__typename?: 'Axis';
|
||||||
|
id: Scalars['ID'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
subjects: Array<Subject>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Category = {
|
||||||
|
__typename?: 'Category';
|
||||||
|
id: Scalars['ID'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
subjects: Array<Subject>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Autogenerated input type of CreateQuestion */
|
||||||
|
export type CreateQuestionInput = {
|
||||||
|
question: QuestionCreateInput;
|
||||||
|
/** A unique identifier for the client performing the mutation. */
|
||||||
|
clientMutationId?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Autogenerated return type of CreateQuestion */
|
||||||
|
export type CreateQuestionPayload = {
|
||||||
|
__typename?: 'CreateQuestionPayload';
|
||||||
|
/** A unique identifier for the client performing the mutation. */
|
||||||
|
clientMutationId?: Maybe<Scalars['String']>;
|
||||||
|
/** Errors encountered during execution of the mutation. */
|
||||||
|
errors: Array<Scalars['String']>;
|
||||||
|
question?: Maybe<Question>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Autogenerated input type of CreateReviewMessage */
|
||||||
|
export type CreateReviewMessageInput = {
|
||||||
|
message: ReviewMessageInput;
|
||||||
|
/** A unique identifier for the client performing the mutation. */
|
||||||
|
clientMutationId?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Autogenerated return type of CreateReviewMessage */
|
||||||
|
export type CreateReviewMessagePayload = {
|
||||||
|
__typename?: 'CreateReviewMessagePayload';
|
||||||
|
/** A unique identifier for the client performing the mutation. */
|
||||||
|
clientMutationId?: Maybe<Scalars['String']>;
|
||||||
|
/** Errors encountered during execution of the mutation. */
|
||||||
|
errors: Array<Scalars['String']>;
|
||||||
|
reviewMessage?: Maybe<ReviewMessage>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DateRangeInput = {
|
||||||
|
startAt: Scalars['ISO8601Date'];
|
||||||
|
endAt: Scalars['ISO8601Date'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Autogenerated input type of DestroyQuestion */
|
||||||
|
export type DestroyQuestionInput = {
|
||||||
|
questionId: Scalars['ID'];
|
||||||
|
/** A unique identifier for the client performing the mutation. */
|
||||||
|
clientMutationId?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Autogenerated return type of DestroyQuestion */
|
||||||
|
export type DestroyQuestionPayload = {
|
||||||
|
__typename?: 'DestroyQuestionPayload';
|
||||||
|
/** A unique identifier for the client performing the mutation. */
|
||||||
|
clientMutationId?: Maybe<Scalars['String']>;
|
||||||
|
deletedQuestionId?: Maybe<Scalars['ID']>;
|
||||||
|
/** Errors encountered during execution of the mutation. */
|
||||||
|
errors: Array<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Autogenerated input type of FinishQuestion */
|
||||||
|
export type FinishQuestionInput = {
|
||||||
|
questionId: Scalars['ID'];
|
||||||
|
/** A unique identifier for the client performing the mutation. */
|
||||||
|
clientMutationId?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Autogenerated return type of FinishQuestion */
|
||||||
|
export type FinishQuestionPayload = {
|
||||||
|
__typename?: 'FinishQuestionPayload';
|
||||||
|
/** A unique identifier for the client performing the mutation. */
|
||||||
|
clientMutationId?: Maybe<Scalars['String']>;
|
||||||
|
/** Errors encountered during execution of the mutation. */
|
||||||
|
errors: Array<Scalars['String']>;
|
||||||
|
question?: Maybe<Question>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type Mutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
createQuestion?: Maybe<CreateQuestionPayload>;
|
||||||
|
createReviewMessage?: Maybe<CreateReviewMessagePayload>;
|
||||||
|
destroyQuestion?: Maybe<DestroyQuestionPayload>;
|
||||||
|
finishQuestion?: Maybe<FinishQuestionPayload>;
|
||||||
|
updateQuestion?: Maybe<UpdateQuestionPayload>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateQuestionArgs = {
|
||||||
|
input: CreateQuestionInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateReviewMessageArgs = {
|
||||||
|
input: CreateReviewMessageInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationDestroyQuestionArgs = {
|
||||||
|
input: DestroyQuestionInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationFinishQuestionArgs = {
|
||||||
|
input: FinishQuestionInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpdateQuestionArgs = {
|
||||||
|
input: UpdateQuestionInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** An object with an ID. */
|
||||||
|
export type Node = {
|
||||||
|
/** ID of the object. */
|
||||||
|
id: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Information about pagination in a connection. */
|
||||||
|
export type PageInfo = {
|
||||||
|
__typename?: 'PageInfo';
|
||||||
|
/** When paginating forwards, the cursor to continue. */
|
||||||
|
endCursor?: Maybe<Scalars['String']>;
|
||||||
|
/** When paginating forwards, are there more items? */
|
||||||
|
hasNextPage: Scalars['Boolean'];
|
||||||
|
/** When paginating backwards, are there more items? */
|
||||||
|
hasPreviousPage: Scalars['Boolean'];
|
||||||
|
/** When paginating backwards, the cursor to continue. */
|
||||||
|
startCursor?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Query = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
currentUser?: Maybe<User>;
|
||||||
|
/** Fetches an object given its ID. */
|
||||||
|
node?: Maybe<Node>;
|
||||||
|
/** Fetches a list of objects given a list of IDs. */
|
||||||
|
nodes: Array<Maybe<Node>>;
|
||||||
|
questions: QuestionConnection;
|
||||||
|
reviewers: UserConnection;
|
||||||
|
subjects: SubjectConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryNodeArgs = {
|
||||||
|
id: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryNodesArgs = {
|
||||||
|
ids: Array<Scalars['ID']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryQuestionsArgs = {
|
||||||
|
after?: Maybe<Scalars['String']>;
|
||||||
|
before?: Maybe<Scalars['String']>;
|
||||||
|
first?: Maybe<Scalars['Int']>;
|
||||||
|
last?: Maybe<Scalars['Int']>;
|
||||||
|
where?: Maybe<QuestionWhereInput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryReviewersArgs = {
|
||||||
|
after?: Maybe<Scalars['String']>;
|
||||||
|
before?: Maybe<Scalars['String']>;
|
||||||
|
first?: Maybe<Scalars['Int']>;
|
||||||
|
last?: Maybe<Scalars['Int']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QuerySubjectsArgs = {
|
||||||
|
after?: Maybe<Scalars['String']>;
|
||||||
|
before?: Maybe<Scalars['String']>;
|
||||||
|
first?: Maybe<Scalars['Int']>;
|
||||||
|
last?: Maybe<Scalars['Int']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Question = Node & {
|
||||||
|
__typename?: 'Question';
|
||||||
|
alternatives: Array<QuestionAlternative>;
|
||||||
|
authorship?: Maybe<Scalars['String']>;
|
||||||
|
authorshipYear?: Maybe<Scalars['String']>;
|
||||||
|
bloomTaxonomy?: Maybe<QuestionBloomTaxonomy>;
|
||||||
|
body?: Maybe<Scalars['String']>;
|
||||||
|
checkType?: Maybe<QuestionCheckType>;
|
||||||
|
createdAt: Scalars['ISO8601DateTime'];
|
||||||
|
difficulty?: Maybe<QuestionDifficulty>;
|
||||||
|
explanation?: Maybe<Scalars['String']>;
|
||||||
|
id: Scalars['ID'];
|
||||||
|
instruction?: Maybe<Scalars['String']>;
|
||||||
|
intention?: Maybe<Scalars['String']>;
|
||||||
|
references?: Maybe<Scalars['String']>;
|
||||||
|
reviewMessages: ReviewMessageConnection;
|
||||||
|
reviewRequests: Array<ReviewRequest>;
|
||||||
|
reviewer?: Maybe<User>;
|
||||||
|
status?: Maybe<QuestionStatus>;
|
||||||
|
subject?: Maybe<Subject>;
|
||||||
|
support?: Maybe<Scalars['String']>;
|
||||||
|
updatedAt: Scalars['ISO8601DateTime'];
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QuestionReviewMessagesArgs = {
|
||||||
|
after?: Maybe<Scalars['String']>;
|
||||||
|
before?: Maybe<Scalars['String']>;
|
||||||
|
first?: Maybe<Scalars['Int']>;
|
||||||
|
last?: Maybe<Scalars['Int']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QuestionAlternative = {
|
||||||
|
__typename?: 'QuestionAlternative';
|
||||||
|
correct: Scalars['Boolean'];
|
||||||
|
text?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QuestionAlternativeInput = {
|
||||||
|
correct?: Maybe<Scalars['Boolean']>;
|
||||||
|
text?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum QuestionBloomTaxonomy {
|
||||||
|
Remember = 'remember',
|
||||||
|
Understand = 'understand',
|
||||||
|
Apply = 'apply',
|
||||||
|
Analyze = 'analyze',
|
||||||
|
Evaluate = 'evaluate',
|
||||||
|
Create = 'create'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum QuestionCheckType {
|
||||||
|
UniqueAnswer = 'unique_answer',
|
||||||
|
IncompleteAffirmation = 'incomplete_affirmation',
|
||||||
|
MultipleAnswer = 'multiple_answer',
|
||||||
|
NegativeFocus = 'negative_focus',
|
||||||
|
AssertionAndReason = 'assertion_and_reason',
|
||||||
|
Gap = 'gap',
|
||||||
|
Interpretation = 'interpretation',
|
||||||
|
Association = 'association',
|
||||||
|
OrderingOrRanking = 'ordering_or_ranking',
|
||||||
|
ConstantAlternatives = 'constant_alternatives'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The connection type for Question. */
|
||||||
|
export type QuestionConnection = {
|
||||||
|
__typename?: 'QuestionConnection';
|
||||||
|
/** A list of edges. */
|
||||||
|
edges: Array<QuestionEdge>;
|
||||||
|
/** A list of nodes. */
|
||||||
|
nodes: Array<Question>;
|
||||||
|
/** Information to aid in pagination. */
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
totalCount: Scalars['Int'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QuestionCreateInput = {
|
||||||
|
instruction: Scalars['String'];
|
||||||
|
support: Scalars['String'];
|
||||||
|
body: Scalars['String'];
|
||||||
|
alternatives: Array<QuestionAlternativeInput>;
|
||||||
|
explanation: Scalars['String'];
|
||||||
|
references: Scalars['String'];
|
||||||
|
authorshipYear: Scalars['String'];
|
||||||
|
authorship: Scalars['String'];
|
||||||
|
intention?: Maybe<Scalars['String']>;
|
||||||
|
status: QuestionStatus;
|
||||||
|
checkType?: Maybe<QuestionCheckType>;
|
||||||
|
difficulty?: Maybe<QuestionDifficulty>;
|
||||||
|
bloomTaxonomy?: Maybe<QuestionBloomTaxonomy>;
|
||||||
|
subjectId?: Maybe<Scalars['ID']>;
|
||||||
|
reviewerUserId?: Maybe<Scalars['ID']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum QuestionDifficulty {
|
||||||
|
Easy = 'easy',
|
||||||
|
Medium = 'medium',
|
||||||
|
Hard = 'hard'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An edge in a connection. */
|
||||||
|
export type QuestionEdge = {
|
||||||
|
__typename?: 'QuestionEdge';
|
||||||
|
/** A cursor for use in pagination. */
|
||||||
|
cursor: Scalars['String'];
|
||||||
|
/** The item at the end of the edge. */
|
||||||
|
node?: Maybe<Question>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum QuestionStatus {
|
||||||
|
Draft = 'DRAFT',
|
||||||
|
WaitingReview = 'WAITING_REVIEW',
|
||||||
|
WithRequestedChanges = 'WITH_REQUESTED_CHANGES',
|
||||||
|
Approved = 'APPROVED',
|
||||||
|
Registered = 'REGISTERED'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionUpdateInput = {
|
||||||
|
instruction: Scalars['String'];
|
||||||
|
support: Scalars['String'];
|
||||||
|
body: Scalars['String'];
|
||||||
|
alternatives: Array<QuestionAlternativeInput>;
|
||||||
|
explanation: Scalars['String'];
|
||||||
|
references: Scalars['String'];
|
||||||
|
authorshipYear: Scalars['String'];
|
||||||
|
authorship: Scalars['String'];
|
||||||
|
intention?: Maybe<Scalars['String']>;
|
||||||
|
status: QuestionStatus;
|
||||||
|
checkType?: Maybe<QuestionCheckType>;
|
||||||
|
difficulty?: Maybe<QuestionDifficulty>;
|
||||||
|
bloomTaxonomy?: Maybe<QuestionBloomTaxonomy>;
|
||||||
|
subjectId?: Maybe<Scalars['ID']>;
|
||||||
|
reviewerUserId?: Maybe<Scalars['ID']>;
|
||||||
|
id: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QuestionWhereInput = {
|
||||||
|
checkType?: Maybe<Array<QuestionCheckType>>;
|
||||||
|
status?: Maybe<Array<QuestionStatus>>;
|
||||||
|
difficulty?: Maybe<Array<QuestionDifficulty>>;
|
||||||
|
bloomTaxonomy?: Maybe<Array<QuestionBloomTaxonomy>>;
|
||||||
|
authorshipYear?: Maybe<Array<Scalars['String']>>;
|
||||||
|
subjectId?: Maybe<Scalars['ID']>;
|
||||||
|
userId?: Maybe<Scalars['ID']>;
|
||||||
|
createDate?: Maybe<DateRangeInput>;
|
||||||
|
unifesoAuthorship?: Maybe<Scalars['Boolean']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReviewMessage = {
|
||||||
|
__typename?: 'ReviewMessage';
|
||||||
|
createdAt: Scalars['ISO8601DateTime'];
|
||||||
|
feedbackType: ReviewMessageFeedbackType;
|
||||||
|
id: Scalars['ID'];
|
||||||
|
question: Question;
|
||||||
|
text: Scalars['String'];
|
||||||
|
updatedAt: Scalars['ISO8601DateTime'];
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The connection type for ReviewMessage. */
|
||||||
|
export type ReviewMessageConnection = {
|
||||||
|
__typename?: 'ReviewMessageConnection';
|
||||||
|
/** A list of edges. */
|
||||||
|
edges: Array<ReviewMessageEdge>;
|
||||||
|
/** A list of nodes. */
|
||||||
|
nodes: Array<ReviewMessage>;
|
||||||
|
/** Information to aid in pagination. */
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
totalCount: Scalars['Int'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** An edge in a connection. */
|
||||||
|
export type ReviewMessageEdge = {
|
||||||
|
__typename?: 'ReviewMessageEdge';
|
||||||
|
/** A cursor for use in pagination. */
|
||||||
|
cursor: Scalars['String'];
|
||||||
|
/** The item at the end of the edge. */
|
||||||
|
node?: Maybe<ReviewMessage>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ReviewMessageFeedbackType {
|
||||||
|
RequestChanges = 'REQUEST_CHANGES',
|
||||||
|
Approve = 'APPROVE',
|
||||||
|
Answer = 'ANSWER'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReviewMessageInput = {
|
||||||
|
feedbackType: ReviewMessageFeedbackType;
|
||||||
|
text: Scalars['String'];
|
||||||
|
questionId: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReviewRequest = {
|
||||||
|
__typename?: 'ReviewRequest';
|
||||||
|
answered: Scalars['Boolean'];
|
||||||
|
id: Scalars['ID'];
|
||||||
|
question: Question;
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The connection type for ReviewRequest. */
|
||||||
|
export type ReviewRequestConnection = {
|
||||||
|
__typename?: 'ReviewRequestConnection';
|
||||||
|
/** A list of edges. */
|
||||||
|
edges: Array<ReviewRequestEdge>;
|
||||||
|
/** A list of nodes. */
|
||||||
|
nodes: Array<ReviewRequest>;
|
||||||
|
/** Information to aid in pagination. */
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
totalCount: Scalars['Int'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** An edge in a connection. */
|
||||||
|
export type ReviewRequestEdge = {
|
||||||
|
__typename?: 'ReviewRequestEdge';
|
||||||
|
/** A cursor for use in pagination. */
|
||||||
|
cursor: Scalars['String'];
|
||||||
|
/** The item at the end of the edge. */
|
||||||
|
node?: Maybe<ReviewRequest>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Subject = {
|
||||||
|
__typename?: 'Subject';
|
||||||
|
axis: Axis;
|
||||||
|
category: Category;
|
||||||
|
id: Scalars['ID'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
questions: QuestionConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type SubjectQuestionsArgs = {
|
||||||
|
after?: Maybe<Scalars['String']>;
|
||||||
|
before?: Maybe<Scalars['String']>;
|
||||||
|
first?: Maybe<Scalars['Int']>;
|
||||||
|
last?: Maybe<Scalars['Int']>;
|
||||||
|
where?: Maybe<QuestionWhereInput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The connection type for Subject. */
|
||||||
|
export type SubjectConnection = {
|
||||||
|
__typename?: 'SubjectConnection';
|
||||||
|
/** A list of edges. */
|
||||||
|
edges: Array<SubjectEdge>;
|
||||||
|
/** A list of nodes. */
|
||||||
|
nodes: Array<Subject>;
|
||||||
|
/** Information to aid in pagination. */
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
totalCount: Scalars['Int'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** An edge in a connection. */
|
||||||
|
export type SubjectEdge = {
|
||||||
|
__typename?: 'SubjectEdge';
|
||||||
|
/** A cursor for use in pagination. */
|
||||||
|
cursor: Scalars['String'];
|
||||||
|
/** The item at the end of the edge. */
|
||||||
|
node?: Maybe<Subject>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Autogenerated input type of UpdateQuestion */
|
||||||
|
export type UpdateQuestionInput = {
|
||||||
|
question: QuestionUpdateInput;
|
||||||
|
/** A unique identifier for the client performing the mutation. */
|
||||||
|
clientMutationId?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Autogenerated return type of UpdateQuestion */
|
||||||
|
export type UpdateQuestionPayload = {
|
||||||
|
__typename?: 'UpdateQuestionPayload';
|
||||||
|
/** A unique identifier for the client performing the mutation. */
|
||||||
|
clientMutationId?: Maybe<Scalars['String']>;
|
||||||
|
/** Errors encountered during execution of the mutation. */
|
||||||
|
errors: Array<Scalars['String']>;
|
||||||
|
question?: Maybe<Question>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
__typename?: 'User';
|
||||||
|
activeReviewRequests: ReviewRequestConnection;
|
||||||
|
avatarUrl?: Maybe<Scalars['String']>;
|
||||||
|
email: Scalars['String'];
|
||||||
|
id: Scalars['ID'];
|
||||||
|
inactiveReviewRequests: ReviewRequestConnection;
|
||||||
|
name: Scalars['String'];
|
||||||
|
roles: Array<UserRole>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type UserActiveReviewRequestsArgs = {
|
||||||
|
after?: Maybe<Scalars['String']>;
|
||||||
|
before?: Maybe<Scalars['String']>;
|
||||||
|
first?: Maybe<Scalars['Int']>;
|
||||||
|
last?: Maybe<Scalars['Int']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type UserInactiveReviewRequestsArgs = {
|
||||||
|
after?: Maybe<Scalars['String']>;
|
||||||
|
before?: Maybe<Scalars['String']>;
|
||||||
|
first?: Maybe<Scalars['Int']>;
|
||||||
|
last?: Maybe<Scalars['Int']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The connection type for User. */
|
||||||
|
export type UserConnection = {
|
||||||
|
__typename?: 'UserConnection';
|
||||||
|
/** A list of edges. */
|
||||||
|
edges: Array<UserEdge>;
|
||||||
|
/** A list of nodes. */
|
||||||
|
nodes: Array<User>;
|
||||||
|
/** Information to aid in pagination. */
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
totalCount: Scalars['Int'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** An edge in a connection. */
|
||||||
|
export type UserEdge = {
|
||||||
|
__typename?: 'UserEdge';
|
||||||
|
/** A cursor for use in pagination. */
|
||||||
|
cursor: Scalars['String'];
|
||||||
|
/** The item at the end of the edge. */
|
||||||
|
node?: Maybe<User>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum UserRole {
|
||||||
|
Admin = 'admin',
|
||||||
|
Teacher = 'teacher',
|
||||||
|
Nde = 'nde',
|
||||||
|
Coordinator = 'coordinator',
|
||||||
|
CenterDirector = 'center_director',
|
||||||
|
ProRector = 'pro_rector'
|
||||||
|
}
|
||||||
@@ -1,14 +1,26 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { ApolloContext } from "./contexts/ApolloContext";
|
import { Provider } from "react-redux";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
const App = () => {
|
import { Appbar } from "./components";
|
||||||
|
import { ApolloContext } from "./contexts";
|
||||||
|
import { PrivateRoutes } from "./routes";
|
||||||
|
import { store } from "./services/store";
|
||||||
|
|
||||||
|
export const App = () => {
|
||||||
return (
|
return (
|
||||||
<ApolloContext>
|
<ApolloContext>
|
||||||
<div>Hello, Rails 7!</div>
|
<Provider store={store}>
|
||||||
</ApolloContext>
|
<BrowserRouter>
|
||||||
|
<Appbar />
|
||||||
|
<PrivateRoutes />
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
</ApolloContext>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
const container = document.getElementById("app");
|
const container = document.getElementById("app");
|
||||||
|
|
||||||
|
|||||||
BIN
app/javascript/assets/images/logoImgUnifeso.png
Normal file
BIN
app/javascript/assets/images/logoImgUnifeso.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
BIN
app/javascript/assets/images/logoNameUnifeso.png
Normal file
BIN
app/javascript/assets/images/logoNameUnifeso.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
18
app/javascript/assets/images/unifeso-logo-branco.svg
Normal file
18
app/javascript/assets/images/unifeso-logo-branco.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="500px" height="181px" viewBox="0 0 5000 1810" preserveAspectRatio="xMidYMid meet">
|
||||||
|
<g id="layer101" fill="#ffffff" stroke="none">
|
||||||
|
<path d="M293 1203 c161 -196 297 -358 303 -360 5 -2 20 17 32 42 l22 45 -202 315 -203 315 -122 0 -123 0 293 -357z"/>
|
||||||
|
<path d="M280 1554 c0 -7 262 -417 351 -550 35 -52 72 -90 135 -139 96 -74 88 -74 133 9 l23 41 -150 320 -149 320 -172 3 c-94 1 -171 0 -171 -4z"/>
|
||||||
|
<path d="M650 1555 c0 -17 305 -654 328 -685 l27 -37 45 21 c37 18 45 26 43 46 -2 23 -30 234 -68 520 -8 58 -15 113 -15 123 0 16 -15 17 -180 17 -99 0 -180 -2 -180 -5z"/>
|
||||||
|
<path d="M1030 1548 c0 -7 20 -164 45 -348 l45 -336 124 -298 c120 -287 132 -314 139 -305 2 2 22 69 44 149 41 143 193 673 278 965 24 83 47 158 50 168 7 16 -14 17 -359 17 -286 0 -366 -3 -366 -12z"/>
|
||||||
|
<path d="M1970 1551 c-79 -17 -126 -57 -162 -136 -20 -46 -22 -68 -26 -287 -2 -131 -1 -249 2 -263 6 -24 10 -25 81 -25 l75 0 0 231 c0 193 3 237 16 265 23 48 59 64 143 64 63 0 74 -3 101 -27 44 -39 49 -78 50 -320 l0 -213 75 0 75 0 0 259 c0 279 -5 311 -55 377 -43 56 -103 78 -230 80 -60 2 -126 -1 -145 -5z"/>
|
||||||
|
<path d="M2386 1545 c-11 -30 -6 -92 11 -131 13 -31 17 -83 21 -254 5 -242 6 -244 81 -288 41 -24 58 -27 161 -31 187 -6 260 23 313 123 21 39 22 52 22 316 l0 275 -69 3 c-49 2 -72 -1 -77 -10 -5 -7 -9 -119 -9 -249 l0 -237 -28 -30 c-55 -59 -199 -52 -246 11 -20 26 -21 45 -26 270 l-5 242 -71 3 c-59 2 -73 0 -78 -13z"/>
|
||||||
|
<path d="M3018 1303 c2 -211 0 -267 -13 -305 -15 -48 -20 -124 -9 -152 9 -23 159 -23 168 0 3 9 6 167 6 353 0 249 -3 340 -12 349 -7 7 -40 12 -77 12 l-66 0 3 -257z"/>
|
||||||
|
<path d="M3197 1553 c-4 -3 -7 -197 -7 -430 0 -464 0 -466 59 -516 37 -31 79 -40 181 -41 l75 -1 0 65 0 65 -60 3 c-32 2 -65 8 -72 14 -8 6 -13 33 -13 64 l0 53 73 3 c66 3 72 5 75 25 2 14 -9 38 -29 63 -29 36 -37 40 -76 40 l-43 0 0 284 c0 156 -3 291 -6 300 -5 13 -22 16 -78 16 -40 0 -76 -3 -79 -7z"/>
|
||||||
|
<path d="M3625 1546 c-92 -29 -139 -70 -177 -151 -21 -45 -23 -65 -23 -195 0 -103 4 -155 14 -180 26 -63 72 -117 124 -146 48 -27 60 -29 157 -29 121 0 160 15 222 83 53 59 73 121 73 227 l0 90 -217 3 -218 2 0 38 c0 20 9 51 20 69 34 57 62 67 190 73 l115 5 0 60 0 60 -120 2 c-75 1 -135 -3 -160 -11z m235 -437 c0 -45 -29 -98 -64 -120 -39 -24 -108 -25 -148 -1 -35 20 -68 79 -68 122 l0 30 140 0 140 0 0 -31z"/>
|
||||||
|
<path d="M3975 1537 c-4 -13 -5 -41 -3 -63 l3 -39 155 -5 c85 -3 161 -9 168 -13 6 -5 12 -26 12 -47 0 -39 -1 -40 -96 -97 -170 -103 -170 -103 -178 -169 -4 -31 -16 -78 -27 -103 l-20 -45 29 -35 c46 -56 103 -81 197 -84 l80 -2 0 65 0 65 -68 3 c-58 3 -69 6 -83 27 -24 37 -7 54 128 135 68 41 137 88 155 105 85 84 52 239 -64 296 -44 22 -63 24 -215 27 l-167 4 -6 -25z"/>
|
||||||
|
<path d="M4573 1546 c-47 -15 -113 -54 -113 -66 0 -4 7 -24 15 -43 19 -45 19 -117 1 -160 -7 -18 -33 -50 -56 -70 l-43 -36 6 -62 c12 -112 74 -206 165 -251 66 -32 215 -33 283 -2 58 27 109 78 140 140 22 46 24 60 24 204 0 143 -2 159 -23 202 -28 55 -85 111 -139 134 -52 22 -205 28 -260 10z m198 -159 c41 -28 57 -75 58 -174 2 -156 -35 -222 -126 -230 -58 -5 -103 19 -130 69 -23 43 -25 235 -4 287 30 69 133 94 202 48z"/>
|
||||||
|
<path d="M2994 717 c-3 -8 -4 -43 -2 -78 l3 -64 85 0 85 0 0 75 0 75 -83 3 c-63 2 -84 0 -88 -11z"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
333
app/javascript/assets/styles/ckeditor-content-styles.css
Normal file
333
app/javascript/assets/styles/ckeditor-content-styles.css
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
/*
|
||||||
|
* CKEditor 5 (v26.0.0) content styles.
|
||||||
|
* Generated on Sun, 14 Mar 2021 13:52:40 GMT.
|
||||||
|
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--ck-color-mention-background: hsla(341, 100%, 30%, 0.1);
|
||||||
|
--ck-color-mention-text: hsl(341, 100%, 30%);
|
||||||
|
--ck-highlight-marker-blue: hsl(201, 97%, 72%);
|
||||||
|
--ck-highlight-marker-green: hsl(120, 93%, 68%);
|
||||||
|
--ck-highlight-marker-pink: hsl(345, 96%, 73%);
|
||||||
|
--ck-highlight-marker-yellow: hsl(60, 97%, 73%);
|
||||||
|
--ck-highlight-pen-green: hsl(112, 100%, 27%);
|
||||||
|
--ck-highlight-pen-red: hsl(0, 85%, 49%);
|
||||||
|
--ck-image-style-spacing: 1.5em;
|
||||||
|
--ck-todo-list-checkmark-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ckeditor5-font/theme/fontsize.css */
|
||||||
|
.ck-content .text-tiny {
|
||||||
|
font-size: .7em;
|
||||||
|
}
|
||||||
|
/* ckeditor5-font/theme/fontsize.css */
|
||||||
|
.ck-content .text-small {
|
||||||
|
font-size: .85em;
|
||||||
|
}
|
||||||
|
/* ckeditor5-font/theme/fontsize.css */
|
||||||
|
.ck-content .text-big {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
/* ckeditor5-font/theme/fontsize.css */
|
||||||
|
.ck-content .text-huge {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
/* ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .marker-yellow {
|
||||||
|
background-color: var(--ck-highlight-marker-yellow);
|
||||||
|
}
|
||||||
|
/* ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .marker-green {
|
||||||
|
background-color: var(--ck-highlight-marker-green);
|
||||||
|
}
|
||||||
|
/* ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .marker-pink {
|
||||||
|
background-color: var(--ck-highlight-marker-pink);
|
||||||
|
}
|
||||||
|
/* ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .marker-blue {
|
||||||
|
background-color: var(--ck-highlight-marker-blue);
|
||||||
|
}
|
||||||
|
/* ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .pen-red {
|
||||||
|
color: var(--ck-highlight-pen-red);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
/* ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .pen-green {
|
||||||
|
color: var(--ck-highlight-pen-green);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
/* ckeditor5-image/theme/imagestyle.css */
|
||||||
|
.ck-content .image-style-side {
|
||||||
|
float: right;
|
||||||
|
margin-left: var(--ck-image-style-spacing);
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
/* ckeditor5-image/theme/imagestyle.css */
|
||||||
|
.ck-content .image-style-align-left {
|
||||||
|
float: left;
|
||||||
|
margin-right: var(--ck-image-style-spacing);
|
||||||
|
}
|
||||||
|
/* ckeditor5-image/theme/imagestyle.css */
|
||||||
|
.ck-content .image-style-align-center {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
/* ckeditor5-image/theme/imagestyle.css */
|
||||||
|
.ck-content .image-style-align-right {
|
||||||
|
float: right;
|
||||||
|
margin-left: var(--ck-image-style-spacing);
|
||||||
|
}
|
||||||
|
/* ckeditor5-code-block/theme/codeblock.css */
|
||||||
|
.ck-content pre {
|
||||||
|
padding: 1em;
|
||||||
|
color: hsl(0, 0%, 20.8%);
|
||||||
|
background: hsla(0, 0%, 78%, 0.3);
|
||||||
|
border: 1px solid hsl(0, 0%, 77%);
|
||||||
|
border-radius: 2px;
|
||||||
|
text-align: left;
|
||||||
|
direction: ltr;
|
||||||
|
tab-size: 4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-style: normal;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
/* ckeditor5-code-block/theme/codeblock.css */
|
||||||
|
.ck-content pre code {
|
||||||
|
background: unset;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
/* ckeditor5-html-embed/theme/htmlembed.css */
|
||||||
|
.ck-content .raw-html-embed {
|
||||||
|
margin: 1em auto;
|
||||||
|
min-width: 15em;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
/* ckeditor5-horizontal-line/theme/horizontalline.css */
|
||||||
|
.ck-content hr {
|
||||||
|
margin: 15px 0;
|
||||||
|
height: 4px;
|
||||||
|
background: hsl(0, 0%, 87%);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
/* ckeditor5-image/theme/image.css */
|
||||||
|
.ck-content .image {
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
text-align: center;
|
||||||
|
/* margin: 1em auto; */
|
||||||
|
}
|
||||||
|
/* ckeditor5-image/theme/image.css */
|
||||||
|
.ck-content .image img {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
/* ckeditor5-image/theme/imagecaption.css */
|
||||||
|
.ck-content .image > figcaption {
|
||||||
|
display: table-caption;
|
||||||
|
caption-side: bottom;
|
||||||
|
word-break: break-word;
|
||||||
|
color: hsl(0, 0%, 20%);
|
||||||
|
background-color: hsl(0, 0%, 97%);
|
||||||
|
padding: .6em;
|
||||||
|
font-size: .75em;
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
|
/* ckeditor5-image/theme/imageresize.css */
|
||||||
|
.ck-content .image.image_resized {
|
||||||
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
/* ckeditor5-image/theme/imageresize.css */
|
||||||
|
.ck-content .image.image_resized img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
/* ckeditor5-image/theme/imageresize.css */
|
||||||
|
.ck-content .image.image_resized > figcaption {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* ckeditor5-block-quote/theme/blockquote.css */
|
||||||
|
.ck-content blockquote {
|
||||||
|
overflow: hidden;
|
||||||
|
padding-right: 1.5em;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
font-style: italic;
|
||||||
|
border-left: solid 5px hsl(0, 0%, 80%);
|
||||||
|
}
|
||||||
|
/* ckeditor5-block-quote/theme/blockquote.css */
|
||||||
|
.ck-content[dir="rtl"] blockquote {
|
||||||
|
border-left: 0;
|
||||||
|
border-right: solid 5px hsl(0, 0%, 80%);
|
||||||
|
}
|
||||||
|
/* ckeditor5-basic-styles/theme/code.css */
|
||||||
|
.ck-content code {
|
||||||
|
background-color: hsla(0, 0%, 78%, 0.3);
|
||||||
|
padding: .15em;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
/* ckeditor5-table/theme/table.css */
|
||||||
|
.ck-content .table {
|
||||||
|
margin: 1em auto;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
/* ckeditor5-table/theme/table.css */
|
||||||
|
.ck-content .table table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px double hsl(0, 0%, 70%);
|
||||||
|
}
|
||||||
|
/* ckeditor5-table/theme/table.css */
|
||||||
|
.ck-content .table table td,
|
||||||
|
.ck-content .table table th {
|
||||||
|
min-width: 2em;
|
||||||
|
padding: .4em;
|
||||||
|
border: 1px solid hsl(0, 0%, 75%);
|
||||||
|
}
|
||||||
|
/* ckeditor5-table/theme/table.css */
|
||||||
|
.ck-content .table table th {
|
||||||
|
font-weight: bold;
|
||||||
|
background: hsla(0, 0%, 0%, 5%);
|
||||||
|
}
|
||||||
|
/* ckeditor5-table/theme/table.css */
|
||||||
|
.ck-content[dir="rtl"] .table th {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
/* ckeditor5-table/theme/table.css */
|
||||||
|
.ck-content[dir="ltr"] .table th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||||
|
.ck-content .page-break {
|
||||||
|
position: relative;
|
||||||
|
clear: both;
|
||||||
|
padding: 5px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||||
|
.ck-content .page-break::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
border-bottom: 2px dashed hsl(0, 0%, 77%);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||||
|
.ck-content .page-break__label {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: .3em .6em;
|
||||||
|
display: block;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border: 1px solid hsl(0, 0%, 77%);
|
||||||
|
border-radius: 2px;
|
||||||
|
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: hsl(0, 0%, 20%);
|
||||||
|
background: hsl(0, 0%, 100%);
|
||||||
|
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
/* ckeditor5-media-embed/theme/mediaembed.css */
|
||||||
|
.ck-content .media {
|
||||||
|
clear: both;
|
||||||
|
margin: 1em 0;
|
||||||
|
display: block;
|
||||||
|
min-width: 15em;
|
||||||
|
}
|
||||||
|
/* ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-content .todo-list {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
/* ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-content .todo-list li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
/* ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-content .todo-list li .todo-list {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
/* ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-content .todo-list .todo-list__label > input {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: var(--ck-todo-list-checkmark-size);
|
||||||
|
height: var(--ck-todo-list-checkmark-size);
|
||||||
|
vertical-align: middle;
|
||||||
|
border: 0;
|
||||||
|
left: -25px;
|
||||||
|
margin-right: -15px;
|
||||||
|
right: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
/* ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-content .todo-list .todo-list__label > input::before {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
content: '';
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid hsl(0, 0%, 20%);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border;
|
||||||
|
}
|
||||||
|
/* ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-content .todo-list .todo-list__label > input::after {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: content-box;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
|
||||||
|
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
|
||||||
|
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
|
||||||
|
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
/* ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-content .todo-list .todo-list__label > input[checked]::before {
|
||||||
|
background: hsl(126, 64%, 41%);
|
||||||
|
border-color: hsl(126, 64%, 41%);
|
||||||
|
}
|
||||||
|
/* ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-content .todo-list .todo-list__label > input[checked]::after {
|
||||||
|
border-color: hsl(0, 0%, 100%);
|
||||||
|
}
|
||||||
|
/* ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-content .todo-list .todo-list__label .todo-list__label__description {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
/* ckeditor5-mention/theme/mention.css */
|
||||||
|
.ck-content .mention {
|
||||||
|
background: var(--ck-color-mention-background);
|
||||||
|
color: var(--ck-color-mention-text);
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||||
|
.ck-content .page-break {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
/* ckeditor5-page-break/theme/pagebreak.css */
|
||||||
|
.ck-content .page-break::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
app/javascript/assets/styles/global.css
Normal file
9
app/javascript/assets/styles/global.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.ck-editor {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, #root {
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f7fafc;
|
||||||
|
font-family: Roboto, 'Times New Roman', Times, serif;
|
||||||
|
}
|
||||||
148272
app/javascript/assets/styles/main.css
Normal file
148272
app/javascript/assets/styles/main.css
Normal file
File diff suppressed because it is too large
Load Diff
5
app/javascript/assets/styles/tailwind.css
Normal file
5
app/javascript/assets/styles/tailwind.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@import "tailwindcss/base";
|
||||||
|
|
||||||
|
@import "tailwindcss/components";
|
||||||
|
|
||||||
|
@import "tailwindcss/utilities";
|
||||||
15
app/javascript/components/Alert/Alert.tsx
Normal file
15
app/javascript/components/Alert/Alert.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Alert: FC<Props> = ({ children }) => (
|
||||||
|
<div
|
||||||
|
className="w-full md:my-2 p-2 bg-red-600 items-center text-red-100 leading-none lg:rounded flex lg:inline-flex"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<span className="flex px-2 py-1 font-bold">Oops!</span>
|
||||||
|
<span className="font-semibold mr-2 text-left flex-auto">{children}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
1
app/javascript/components/Alert/index.ts
Normal file
1
app/javascript/components/Alert/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Alert } from "./Alert";
|
||||||
38
app/javascript/components/AlertV2/AlertV2.tsx
Normal file
38
app/javascript/components/AlertV2/AlertV2.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import { MdDone, MdError, MdInfo, MdWarning } from 'react-icons/md'
|
||||||
|
|
||||||
|
type AlertSeverity = 'error' | 'warning' | 'info' | 'success'
|
||||||
|
|
||||||
|
const ICONS = {
|
||||||
|
error: <MdError />,
|
||||||
|
warning: <MdWarning />,
|
||||||
|
info: <MdInfo />,
|
||||||
|
success: <MdDone />
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLOR_CLASSES = {
|
||||||
|
error: 'bg-red-300 border-red-600 text-red-800',
|
||||||
|
warning: 'bg-orange-300 border-orange-600 text-orange-800',
|
||||||
|
info: 'bg-blue-300 border-blue-600 text-blue-800',
|
||||||
|
success: 'bg-green-300 border-green-600 text-green-800',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
severity?: AlertSeverity
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AlertV2: FC<Props> = ({
|
||||||
|
severity = 'info',
|
||||||
|
text = '',
|
||||||
|
}) => (
|
||||||
|
<div className={`flex rounded shadow p-4 mx-auto my-2 ${COLOR_CLASSES[severity]}`}>
|
||||||
|
<div className="text-xl my-auto pr-2">
|
||||||
|
{ICONS[severity]}
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
2
app/javascript/components/AlertV2/index.ts
Normal file
2
app/javascript/components/AlertV2/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { AlertV2 } from "./AlertV2";
|
||||||
|
export type { Props as AlertV2Props } from "./AlertV2";
|
||||||
221
app/javascript/components/Appbar/Appbar.tsx
Normal file
221
app/javascript/components/Appbar/Appbar.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import React, { FC, Fragment, useState } from 'react'
|
||||||
|
import { useHistory, useLocation } from 'react-router';
|
||||||
|
import { Menu, Transition } from '@headlessui/react'
|
||||||
|
import { ChartBarIcon, ClipboardListIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
|
import { Dialog } from '../Dialog'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useCurrentUser } from '../../contexts';
|
||||||
|
import { RootState } from '../../services/store';
|
||||||
|
import { classNames } from '../../utils';
|
||||||
|
import { DashboardRoutePaths, QuestionRoutePaths, SessionRoutePaths } from '../../routes'
|
||||||
|
import { turnOff } from '../../services/store/unsavedChanges';
|
||||||
|
import { CurrentUserAvatar } from "../CurrentUserAvatar";
|
||||||
|
|
||||||
|
const UserMenu: FC = () => {
|
||||||
|
const { user } = useCurrentUser();
|
||||||
|
const history = useHistory();
|
||||||
|
const [confirmLogout, setConfirmLogout] = useState(false)
|
||||||
|
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const doLogout = () => {
|
||||||
|
setConfirmLogout(false)
|
||||||
|
dispatch(turnOff())
|
||||||
|
history.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
if (unsavedChanges && !confirmLogout) {
|
||||||
|
setConfirmLogout(true)
|
||||||
|
} else {
|
||||||
|
doLogout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [newPath, setNewPath] = useState<string>()
|
||||||
|
|
||||||
|
const handleForcedRedirect = () => {
|
||||||
|
if (!newPath) return
|
||||||
|
|
||||||
|
dispatch(turnOff())
|
||||||
|
setNewPath(undefined)
|
||||||
|
history.push(newPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLinkClick = (pathname: string) => {
|
||||||
|
if (unsavedChanges) {
|
||||||
|
setNewPath(pathname)
|
||||||
|
} else {
|
||||||
|
history.push(pathname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
onClick: () => { handleLinkClick(SessionRoutePaths.show) },
|
||||||
|
label: 'Perfil'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onClick: handleLogout,
|
||||||
|
label: 'Sair'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
isOpen={!!newPath}
|
||||||
|
setIsOpen={(value) => setNewPath(value ? newPath : undefined)}
|
||||||
|
onConfirmation={handleForcedRedirect}
|
||||||
|
title="Modificações não Salvas"
|
||||||
|
text="Todas as alterações serão descartadas. Deseja continuar?"
|
||||||
|
/>
|
||||||
|
<Dialog
|
||||||
|
isOpen={confirmLogout}
|
||||||
|
setIsOpen={setConfirmLogout}
|
||||||
|
onConfirmation={handleLogout}
|
||||||
|
title="Modificações não Salvas"
|
||||||
|
text="Todas as alterações serão descartadas. Deseja continuar?"
|
||||||
|
/>
|
||||||
|
<Menu as="div" className="relative h-full">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Menu.Button
|
||||||
|
className="h-full flex flex-row px-2 items-center hover:bg-primary-dark text-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
||||||
|
>
|
||||||
|
<span className="hidden md:block pr-2">
|
||||||
|
{user?.name}
|
||||||
|
</span>
|
||||||
|
<div className="w-12">
|
||||||
|
<CurrentUserAvatar />
|
||||||
|
</div>
|
||||||
|
</Menu.Button>
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items
|
||||||
|
static
|
||||||
|
className="z-50 origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none cursor-pointer"
|
||||||
|
>
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<Menu.Item key={`menu-item-${item.label}`} onClick={item.onClick}>
|
||||||
|
{({ active }) => (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
active ? 'bg-gray-100' : '',
|
||||||
|
'block px-4 py-2 text-sm text-gray-900'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Links: FC = () => {
|
||||||
|
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const location = useLocation()
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
const [newPath, setNewPath] = useState<string>()
|
||||||
|
|
||||||
|
const handleForcedRedirect = () => {
|
||||||
|
if (!newPath) return
|
||||||
|
|
||||||
|
dispatch(turnOff())
|
||||||
|
setNewPath(undefined)
|
||||||
|
history.push(newPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLinkClick = (pathname: string) => {
|
||||||
|
if (unsavedChanges) {
|
||||||
|
setNewPath(pathname)
|
||||||
|
} else {
|
||||||
|
history.push(pathname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const links = [{
|
||||||
|
icon: <ChartBarIcon className="w-6" />,
|
||||||
|
tabel: 'Painel',
|
||||||
|
pathname: DashboardRoutePaths.index,
|
||||||
|
isCurrent: location.pathname.includes('dashboard'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ClipboardListIcon className="w-6" />,
|
||||||
|
tabel: 'Edição',
|
||||||
|
pathname: QuestionRoutePaths.index,
|
||||||
|
isCurrent: location.pathname.includes('question'),
|
||||||
|
}]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
isOpen={!!newPath}
|
||||||
|
setIsOpen={(value) => setNewPath(value ? newPath : undefined)}
|
||||||
|
onConfirmation={handleForcedRedirect}
|
||||||
|
title="Modificações não Salvas"
|
||||||
|
text="Todas as alterações serão descartadas. Deseja continuar?"
|
||||||
|
/>
|
||||||
|
<div className="h-full flex items-center pl-4">
|
||||||
|
{links.map((link) => (
|
||||||
|
<button
|
||||||
|
className={`h-full flex items-center px-2 mx-2 text-gray-300 hover:bg-primary-dark ${link.isCurrent ? 'underline bg-primary-dark' : ''}`}
|
||||||
|
key={`navbar-link-${link.pathname}`}
|
||||||
|
onClick={() => handleLinkClick(link.pathname)}
|
||||||
|
>
|
||||||
|
<span className="pr-2 ">
|
||||||
|
{link.icon}
|
||||||
|
</span>
|
||||||
|
{link.tabel}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Logo: FC = () => (
|
||||||
|
<div className="h-full grid place-items-center">
|
||||||
|
<img
|
||||||
|
alt="Símbolo do Unifeso"
|
||||||
|
className="hidden md:block h-12 w-auto"
|
||||||
|
src={'unifesoLogo'}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
alt="Logotipo do Unifeso"
|
||||||
|
className="md:hidden h-12 w-auto"
|
||||||
|
src={'unifesoLogoCompact'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Appbar = () => {
|
||||||
|
return (
|
||||||
|
<div className="px-4 bg-primary-normal flex items-center justify-between h-16 shadow-md">
|
||||||
|
<div className="flex h-full">
|
||||||
|
<Logo />
|
||||||
|
<Links />
|
||||||
|
</div>
|
||||||
|
<UserMenu />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
app/javascript/components/Appbar/index.ts
Normal file
1
app/javascript/components/Appbar/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Appbar";
|
||||||
63
app/javascript/components/AvatarEditor/AvatarEditor.tsx
Normal file
63
app/javascript/components/AvatarEditor/AvatarEditor.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React, { FC, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { Alert } from "../Alert";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import { PhotoCrop } from "./PhotoCrop";
|
||||||
|
import { useCurrentUser } from "../../contexts";
|
||||||
|
import { Modal } from "../Modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean
|
||||||
|
setIsOpen: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AvatarEditor: FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||||
|
const [croppedImage, setCroppedImage] = useState<any>()
|
||||||
|
const [alert, setAlert] = useState<boolean>()
|
||||||
|
const { refetch, authToken } = useCurrentUser()
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.defaults.headers.common.Authorization = `Bearer ${authToken}`;
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
instance
|
||||||
|
.post("/update_avatar", {
|
||||||
|
upload: croppedImage,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
setIsOpen(false)
|
||||||
|
refetch()
|
||||||
|
} else {
|
||||||
|
setAlert(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setAlert(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Alterar Imagem de Perfil"
|
||||||
|
isOpen={isOpen}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
buttons={
|
||||||
|
<>
|
||||||
|
<Button onClick={() => setIsOpen(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" onClick={() => onSubmit()}>
|
||||||
|
Salvar
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{alert && <Alert>Algo deu errado, tente novamente mais tarde.</Alert>}
|
||||||
|
<PhotoCrop callback={setCroppedImage} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
49
app/javascript/components/AvatarEditor/PhotoCrop.tsx
Normal file
49
app/javascript/components/AvatarEditor/PhotoCrop.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { FC, useState } from "react";
|
||||||
|
import PhotoCropper from "react-avatar-edit";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
callback: (value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const borderStyle: React.CSSProperties = {
|
||||||
|
textAlign: 'center',
|
||||||
|
margin: 'auto',
|
||||||
|
borderStyle: 'dotted',
|
||||||
|
borderWidth: '0.3rem',
|
||||||
|
borderRadius: '0.3rem',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PhotoCrop: FC<Props> = ({ callback }) => {
|
||||||
|
const [result, setResult] = useState<any>();
|
||||||
|
const onCrop = (cropped: any) => {
|
||||||
|
setResult(cropped);
|
||||||
|
callback(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
setResult(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBeforeFileLoad = (elem: any) => {
|
||||||
|
if (elem.target.files[0].size > 1000000) {
|
||||||
|
elem.target.value = "";
|
||||||
|
alert("A imagem selecionada é grande de mais!")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const dimention = 300;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PhotoCropper
|
||||||
|
borderStyle={borderStyle}
|
||||||
|
label="Escolha uma imagem"
|
||||||
|
width={dimention}
|
||||||
|
height={dimention}
|
||||||
|
imageWidth={dimention}
|
||||||
|
imageHeight={dimention}
|
||||||
|
onCrop={(e) => onCrop(e)}
|
||||||
|
onClose={() => onClose()}
|
||||||
|
onBeforeFileLoad={onBeforeFileLoad}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
app/javascript/components/AvatarEditor/index.ts
Normal file
1
app/javascript/components/AvatarEditor/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { AvatarEditor } from "./AvatarEditor";
|
||||||
63
app/javascript/components/Button/Button.tsx
Normal file
63
app/javascript/components/Button/Button.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const styleClasses = {
|
||||||
|
primary: "bg-primary-normal hover:bg-primary-dark text-white",
|
||||||
|
secondary: "bg-gray-200 hover:bg-gray-400 text-gray-800",
|
||||||
|
disabled: "bg-gray-200 text-gray-600 cursor-not-allowed shadow-none hover:shadow-none",
|
||||||
|
tertiary: "shadow-none hover:shadow-none drop-shadow-sm text-gray-900 hover:text-gray-600",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ButtonProps = {
|
||||||
|
type?: 'default' | 'primary' | 'tertiary';
|
||||||
|
className?: string;
|
||||||
|
children?: string | JSX.Element;
|
||||||
|
htmlType?: 'submit' | 'button' | 'reset';
|
||||||
|
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonBase: React.ForwardRefRenderFunction<unknown, ButtonProps> = (props, ref) => {
|
||||||
|
const {
|
||||||
|
type = 'default',
|
||||||
|
className = '',
|
||||||
|
htmlType = 'button',
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
const buttonRef = (ref as any) || React.createRef<HTMLElement>()
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
|
||||||
|
if (htmlType !== 'submit') {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disabled || !onClick) return
|
||||||
|
|
||||||
|
onClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraClasses = () => {
|
||||||
|
if (disabled) return styleClasses.disabled
|
||||||
|
|
||||||
|
if (type === 'primary') return styleClasses.primary
|
||||||
|
|
||||||
|
if (type === 'tertiary') return styleClasses.tertiary
|
||||||
|
|
||||||
|
return styleClasses.secondary
|
||||||
|
}
|
||||||
|
|
||||||
|
return <button
|
||||||
|
{...rest}
|
||||||
|
type={htmlType}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`transition duration-300 ease-in-out block text-center cursor-pointer p-2 px-8 rounded shadow-lg hover:shadow-lg ${extraClasses()} ${className}`}
|
||||||
|
onClick={handleClick}
|
||||||
|
ref={buttonRef}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button = React.forwardRef<unknown, ButtonProps>(ButtonBase)
|
||||||
1
app/javascript/components/Button/index.ts
Normal file
1
app/javascript/components/Button/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Button } from "./Button";
|
||||||
23
app/javascript/components/Card/Card.tsx
Normal file
23
app/javascript/components/Card/Card.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
action?: () => void
|
||||||
|
children: any
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
export const Card: FC<Props> = ({
|
||||||
|
title, action, children, className = '',
|
||||||
|
}) => (
|
||||||
|
<div className={`bg-white md:rounded shadow-sm border border-gray-300 w-full ${className}`}>
|
||||||
|
<div className="border-b border-gray-300 bg-gray-100 md:rounded-t p-2 shadow-sm flex items-center">
|
||||||
|
<span className="text-lg text-gray-800 flex-grow">{title}</span>
|
||||||
|
{
|
||||||
|
action ? action() : null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="p-4 h-full">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
1
app/javascript/components/Card/index.ts
Normal file
1
app/javascript/components/Card/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Card } from "./Card";
|
||||||
19
app/javascript/components/CardGrid/CardGrid.tsx
Normal file
19
app/javascript/components/CardGrid/CardGrid.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const Grid = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 1rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
children: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CardGrid: FC<Props> = ({ children, className }) => (
|
||||||
|
<Grid className={className}>
|
||||||
|
{children}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
1
app/javascript/components/CardGrid/index.ts
Normal file
1
app/javascript/components/CardGrid/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { CardGrid } from "./CardGrid";
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React, {FC} from 'react'
|
||||||
|
|
||||||
|
import {useCurrentUser} from "../../contexts";
|
||||||
|
import {UserAvatar} from "../UserAvatar";
|
||||||
|
|
||||||
|
export const CurrentUserAvatar: FC = () => {
|
||||||
|
const {user} = useCurrentUser()
|
||||||
|
|
||||||
|
if (!user) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserAvatar user={user}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
app/javascript/components/CurrentUserAvatar/index.ts
Normal file
1
app/javascript/components/CurrentUserAvatar/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './CurrentUserAvatar'
|
||||||
46
app/javascript/components/Dialog/Dialog.tsx
Normal file
46
app/javascript/components/Dialog/Dialog.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { FC } from "react"
|
||||||
|
import { Button } from "../Button"
|
||||||
|
import { Modal } from '../Modal'
|
||||||
|
|
||||||
|
type DialogType = 'confirmation' | 'notice'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type?: DialogType
|
||||||
|
title: string
|
||||||
|
isOpen?: boolean
|
||||||
|
text?: any
|
||||||
|
setIsOpen: (state: boolean) => void
|
||||||
|
onConfirmation: () => void
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Dialog: FC<Props> = ({
|
||||||
|
type = 'confirmation',
|
||||||
|
title,
|
||||||
|
isOpen: open = false,
|
||||||
|
setIsOpen,
|
||||||
|
onConfirmation,
|
||||||
|
text,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={title}
|
||||||
|
isOpen={open}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
buttons={
|
||||||
|
<>
|
||||||
|
{type === 'confirmation' &&
|
||||||
|
<Button onClick={() => setIsOpen(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
<Button type="primary" onClick={onConfirmation}>
|
||||||
|
Confirmar
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
1
app/javascript/components/Dialog/index.ts
Normal file
1
app/javascript/components/Dialog/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Dialog";
|
||||||
62
app/javascript/components/Disclosures/Disclosures.tsx
Normal file
62
app/javascript/components/Disclosures/Disclosures.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import React, { FC, Fragment } from 'react'
|
||||||
|
import { Disclosure, Transition } from '@headlessui/react'
|
||||||
|
import { ChevronDownIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
|
type Item = {
|
||||||
|
title?: string | JSX.Element
|
||||||
|
body?: string | JSX.Element
|
||||||
|
icon?: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: Item[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Disclosures: FC<Props> = ({
|
||||||
|
items
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full grid grid-cols-1 gap-3">
|
||||||
|
<div className="w-full max-w-md p-2 mx-auto bg-white rounded-2xl">
|
||||||
|
{items.map((item) => (
|
||||||
|
<Disclosure as="div" className="my-2 rounded border">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button as={Fragment}>
|
||||||
|
<button className="flex p-2 bg-gray-200 w-full justify-between">
|
||||||
|
<div className="grid place-items-center">
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
|
<span className="pl-2">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={`${open ? 'transform rotate-180' : ''} w-5 h-5 text-gray-800`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
enter="transition duration-100 ease-out"
|
||||||
|
enterFrom="transform scale-95 opacity-0"
|
||||||
|
enterTo="transform scale-100 opacity-100"
|
||||||
|
leave="transition duration-75 ease-out"
|
||||||
|
leaveFrom="transform scale-100 opacity-100"
|
||||||
|
leaveTo="transform scale-95 opacity-0"
|
||||||
|
>
|
||||||
|
|
||||||
|
<Disclosure.Panel
|
||||||
|
className="p-2 bg-gray-100"
|
||||||
|
>
|
||||||
|
{item.body ?? 'Nenhum comentário.'}
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
app/javascript/components/Disclosures/index.ts
Normal file
1
app/javascript/components/Disclosures/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Disclosures";
|
||||||
39
app/javascript/components/Input/Input.tsx
Normal file
39
app/javascript/components/Input/Input.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React, {ChangeEvent} from 'react'
|
||||||
|
import {v4 as uuid} from 'uuid'
|
||||||
|
|
||||||
|
export type Props = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonBase: React.ForwardRefRenderFunction<unknown, Props> = (props, ref) => {
|
||||||
|
const {
|
||||||
|
className = '',
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
const inputRef = (ref as any) || React.createRef<HTMLElement>()
|
||||||
|
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!onChange) return
|
||||||
|
|
||||||
|
onChange(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = uuid()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{label ?? <label htmlFor={id}>{label}</label>}
|
||||||
|
<input
|
||||||
|
{...rest}
|
||||||
|
id={id}
|
||||||
|
className={`block rounded p-1 w-full border-gray-400 border shadow-sm ${className}`}
|
||||||
|
onChange={handleChange}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Input = React.forwardRef<unknown, Props>(ButtonBase)
|
||||||
1
app/javascript/components/Input/index.ts
Normal file
1
app/javascript/components/Input/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Input'
|
||||||
19
app/javascript/components/InputGroup/InputGroup.tsx
Normal file
19
app/javascript/components/InputGroup/InputGroup.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const StyledInputGroup = styled.div`
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputGroup: FC<Props> = ({ children }) => (
|
||||||
|
<StyledInputGroup className="mt-4 mb-2">{children}</StyledInputGroup>
|
||||||
|
);
|
||||||
1
app/javascript/components/InputGroup/index.ts
Normal file
1
app/javascript/components/InputGroup/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { InputGroup } from "./InputGroup";
|
||||||
57
app/javascript/components/List/List.tsx
Normal file
57
app/javascript/components/List/List.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
|
||||||
|
type ListItemIconProps = {
|
||||||
|
icon: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItemIcon: FC<ListItemIconProps> = ({ icon }) => {
|
||||||
|
return (
|
||||||
|
<div className="grid place-items-center pr-3">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListItemTextProps = {
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItemText: FC<ListItemTextProps> = ({ text }) => {
|
||||||
|
return (
|
||||||
|
<div className="pl-2">
|
||||||
|
<p>
|
||||||
|
{text ?? ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListItemProps = {
|
||||||
|
icon?: JSX.Element
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ListItem: FC<ListItemProps> = ({ icon, text, children }) => {
|
||||||
|
return (
|
||||||
|
<li className="flex py-2 border-t border-b border-gray-200">
|
||||||
|
{icon && <ListItemIcon icon={icon} />}
|
||||||
|
{text && <ListItemText text={text} />}
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListProps = {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const List: FC<ListProps> = ({
|
||||||
|
className = '',
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ul className={`list-none p-0 m-0 ${className}`}>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
app/javascript/components/List/index.ts
Normal file
1
app/javascript/components/List/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./List";
|
||||||
82
app/javascript/components/Modal/Modal.tsx
Normal file
82
app/javascript/components/Modal/Modal.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { FC, Fragment } from 'react'
|
||||||
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean
|
||||||
|
setIsOpen: (state: boolean) => void
|
||||||
|
buttons?: any,
|
||||||
|
title: string,
|
||||||
|
className?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Modal: FC<Props> = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
children,
|
||||||
|
buttons,
|
||||||
|
title,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
as="div"
|
||||||
|
className={`fixed inset-0 z-10 overflow-y-auto ${className}`}
|
||||||
|
onClose={closeModal}
|
||||||
|
>
|
||||||
|
<div className="min-h-screen px-4 text-center">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 bg-gray-900 bg-opacity-50" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className="inline-block h-screen align-middle"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
​
|
||||||
|
</span>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<div className="inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-gray-900 mt-2 mb-4"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{buttons &&
|
||||||
|
<div className="mt-4 grid grid-flow-col gap-3">
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
app/javascript/components/Modal/index.ts
Normal file
1
app/javascript/components/Modal/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Modal";
|
||||||
106
app/javascript/components/Navegator/Navegator.tsx
Normal file
106
app/javascript/components/Navegator/Navegator.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import React, { FC, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { Link, useHistory } from "react-router-dom";
|
||||||
|
import { FaHome, FaPlus } from "react-icons/fa";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { turnOff } from "../../services/store/unsavedChanges";
|
||||||
|
import { RootState } from "../../services/store";
|
||||||
|
import { Dialog } from "../Dialog";
|
||||||
|
import {QuestionRoutePaths} from "../../routes";
|
||||||
|
|
||||||
|
const HorizontalMenu = styled.ul`
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
& > li {
|
||||||
|
display: inline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
& > li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
& > li > div {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-right: 2rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ItemProps = {
|
||||||
|
className?: string
|
||||||
|
children?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const Item: FC<ItemProps> = ({ children, className }) => (
|
||||||
|
<div className={`hover:text-white ${className || ""}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
home?: boolean
|
||||||
|
newQuestion?: boolean
|
||||||
|
children?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Navigator: FC<Props> = ({
|
||||||
|
home = false, newQuestion = false, children,
|
||||||
|
}) => {
|
||||||
|
const [confirmLeaveDialog, setConfirmLeaveDialog] = useState(false);
|
||||||
|
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const confirmLeave = () => {
|
||||||
|
dispatch(turnOff());
|
||||||
|
history.push(QuestionRoutePaths.index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
if (unsavedChanges) {
|
||||||
|
setConfirmLeaveDialog(true);
|
||||||
|
} else {
|
||||||
|
confirmLeave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
isOpen={confirmLeaveDialog}
|
||||||
|
setIsOpen={(value) => setConfirmLeaveDialog(value)}
|
||||||
|
onConfirmation={confirmLeave}
|
||||||
|
title="Modificações não Salvas"
|
||||||
|
text="Todas as alterações serão descartadas. Deseja continuar?"
|
||||||
|
/>
|
||||||
|
<div className="flex p-1 text-md px-2 sm:px-8 text-gray-400 bg-primary-dark shadow-md" style={{ maxHeight: "34.4px" }}>
|
||||||
|
<HorizontalMenu className="list-none">
|
||||||
|
{home
|
||||||
|
&& (
|
||||||
|
<Item>
|
||||||
|
<button onClick={() => goHome()} className="flex">
|
||||||
|
<FaHome className="my-auto" />
|
||||||
|
<span className="pl-3">Início</span>
|
||||||
|
</button>
|
||||||
|
</Item>
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
(newQuestion) ? (
|
||||||
|
<Item>
|
||||||
|
<Link to="/questions/new" className="flex">
|
||||||
|
<FaPlus className="my-auto" />
|
||||||
|
<span className="pl-3">Nova Questão</span>
|
||||||
|
</Link>
|
||||||
|
</Item>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
{children}
|
||||||
|
</HorizontalMenu>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
app/javascript/components/Navegator/index.ts
Normal file
1
app/javascript/components/Navegator/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Navigator } from "./Navegator";
|
||||||
27
app/javascript/components/UserAvatar/UserAvatar.tsx
Normal file
27
app/javascript/components/UserAvatar/UserAvatar.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React, {FC} from "react";
|
||||||
|
import {User} from "../../__generated__/graphql-schema";
|
||||||
|
import BoringAvatar from "boring-avatars";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: User
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserAvatar: FC<Props> = ({user, className}) => {
|
||||||
|
return (
|
||||||
|
<div className={`rounded-full border-2 border-primary-light shadow ${className || ''}`}>
|
||||||
|
{user.avatarUrl ?
|
||||||
|
<img
|
||||||
|
src={`${process.env.REACT_APP_BACKEND_URL}/${user.avatarUrl}`}
|
||||||
|
alt={`Avatar do usuário ${user.name}`}
|
||||||
|
/>
|
||||||
|
: <BoringAvatar
|
||||||
|
size={"100%"}
|
||||||
|
name={user.name}
|
||||||
|
variant="pixel"
|
||||||
|
colors={["#595F72", "#575D90", "#84A07C", "#C3D350", "#E6F14A"]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
1
app/javascript/components/UserAvatar/index.ts
Normal file
1
app/javascript/components/UserAvatar/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { UserAvatar } from "./UserAvatar";
|
||||||
16
app/javascript/components/index.ts
Normal file
16
app/javascript/components/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export * from "./Alert";
|
||||||
|
export * from "./AlertV2";
|
||||||
|
export * from "./Button";
|
||||||
|
export * from "./Card";
|
||||||
|
export * from "./CardGrid";
|
||||||
|
export * from "./InputGroup";
|
||||||
|
export * from "./AvatarEditor";
|
||||||
|
export * from "./Navegator";
|
||||||
|
export * from "./UserAvatar";
|
||||||
|
export * from "./Dialog";
|
||||||
|
export * from "./Appbar";
|
||||||
|
export * from "./Modal";
|
||||||
|
export * from "./List";
|
||||||
|
export * from "./Disclosures";
|
||||||
|
export * from './CurrentUserAvatar'
|
||||||
|
export * from './Input'
|
||||||
75
app/javascript/contexts/UserContext.tsx
Normal file
75
app/javascript/contexts/UserContext.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import React, {
|
||||||
|
createContext, useContext, useState, FC
|
||||||
|
} from "react";
|
||||||
|
import { useQuery, gql } from "@apollo/client";
|
||||||
|
|
||||||
|
import { Query, UserRole } from "../__generated__/graphql-schema";
|
||||||
|
import { UnauthorizedAccess } from "../pages/session";
|
||||||
|
import { Loading } from "../pages/shared";
|
||||||
|
|
||||||
|
export type UserContext = {
|
||||||
|
user?: Query['currentUser']
|
||||||
|
refetch: () => void
|
||||||
|
isOnlyTeacher: boolean
|
||||||
|
authToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Context = createContext<UserContext>({
|
||||||
|
refetch: () => {
|
||||||
|
},
|
||||||
|
isOnlyTeacher: false,
|
||||||
|
authToken: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useCurrentUser = (): UserContext => {
|
||||||
|
const context = useContext(Context);
|
||||||
|
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error("You probably forgot to put <UserContext>.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CurrentUserQuery = gql`
|
||||||
|
query CurrentUserQuery {
|
||||||
|
currentUser {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
email
|
||||||
|
avatarUrl
|
||||||
|
roles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: any
|
||||||
|
authToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserContext: FC<Props> = ({ children, authToken }) => {
|
||||||
|
const [user, setUser] = useState<Query['currentUser']>();
|
||||||
|
const isOnlyTeacher = !!(user?.roles.includes(UserRole.Teacher) && user?.roles.length === 1)
|
||||||
|
|
||||||
|
const { refetch: refetchUserQuery, loading } = useQuery<Query>(CurrentUserQuery, {
|
||||||
|
onCompleted: ({ currentUser }) => {
|
||||||
|
setUser(currentUser)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const refetch = async () => {
|
||||||
|
const { data: { currentUser } } = await refetchUserQuery()
|
||||||
|
setUser(currentUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return <Loading />
|
||||||
|
|
||||||
|
if (!user) return <UnauthorizedAccess />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={{ user, refetch, isOnlyTeacher, authToken }}>
|
||||||
|
{children}
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
app/javascript/contexts/index.ts
Normal file
2
app/javascript/contexts/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./ApolloContext";
|
||||||
|
export * from "./UserContext";
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Application } from "@hotwired/stimulus"
|
|
||||||
|
|
||||||
const application = Application.start()
|
|
||||||
|
|
||||||
// Configure Stimulus development experience
|
|
||||||
application.debug = false
|
|
||||||
window.Stimulus = application
|
|
||||||
|
|
||||||
export { application }
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
connect() {
|
|
||||||
this.element.textContent = "Hello World!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// Import and register all your controllers from the importmap under controllers/*
|
|
||||||
|
|
||||||
import { application } from "controllers/application"
|
|
||||||
|
|
||||||
// Eager load all controllers defined in the import map under controllers/**/*_controller
|
|
||||||
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
||||||
eagerLoadControllersFrom("controllers", application)
|
|
||||||
|
|
||||||
// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
|
|
||||||
// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
||||||
// lazyLoadControllersFrom("controllers", application)
|
|
||||||
24
app/javascript/pages/dashboard/Dashboard.tsx
Normal file
24
app/javascript/pages/dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React, {FC,} from 'react'
|
||||||
|
|
||||||
|
import {DashboardProvider} from './DashboardContext'
|
||||||
|
import {
|
||||||
|
QuestionsBySubject,
|
||||||
|
QuestionByBloomTaxonomy,
|
||||||
|
QuestionsByDifficulty,
|
||||||
|
QuestionByCheckType,
|
||||||
|
} from './charts'
|
||||||
|
import {Filters} from './Filters'
|
||||||
|
|
||||||
|
export const Dashboard: FC = () => (
|
||||||
|
<DashboardProvider>
|
||||||
|
<main className="max-h-screen sm:px-8 gap-2 pt-2 sm:pt-4">
|
||||||
|
<Filters/>
|
||||||
|
<div className="pt-3 grid gap-2 grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 3xl:grid-cols-4">
|
||||||
|
<QuestionsBySubject/>
|
||||||
|
<QuestionByBloomTaxonomy/>
|
||||||
|
<QuestionsByDifficulty/>
|
||||||
|
<QuestionByCheckType/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</DashboardProvider>
|
||||||
|
)
|
||||||
47
app/javascript/pages/dashboard/DashboardContext.tsx
Normal file
47
app/javascript/pages/dashboard/DashboardContext.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
FC,
|
||||||
|
useContext,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
} from 'react'
|
||||||
|
import {QuestionWhereInput} from '../../__generated__/graphql-schema'
|
||||||
|
import {UserContext, useCurrentUser} from "../../contexts";
|
||||||
|
|
||||||
|
type ProviderValue = {
|
||||||
|
where: QuestionWhereInput
|
||||||
|
setWhere: Dispatch<SetStateAction<QuestionWhereInput>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardContext = createContext<ProviderValue | null>(null)
|
||||||
|
|
||||||
|
export const useDashboardContext = () => {
|
||||||
|
const context = useContext(DashboardContext)
|
||||||
|
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error('You probably forgot to put <DashboardProvider>.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export const whereDefaultState = (userContext: UserContext) => (
|
||||||
|
userContext.isOnlyTeacher ? {userId: userContext.user?.id} : {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const DashboardProvider: FC = ({children}) => {
|
||||||
|
const userContext = useCurrentUser()
|
||||||
|
const [where, setWhere] = useState<QuestionWhereInput>(whereDefaultState(userContext))
|
||||||
|
const providerValue = useMemo(() => ({where, setWhere}), [
|
||||||
|
where,
|
||||||
|
setWhere,
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardContext.Provider value={providerValue}>
|
||||||
|
{children}
|
||||||
|
</DashboardContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
155
app/javascript/pages/dashboard/Filters.tsx
Normal file
155
app/javascript/pages/dashboard/Filters.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import React, {FC, Fragment} from 'react'
|
||||||
|
import {Disclosure, Transition} from "@headlessui/react"
|
||||||
|
import {ChevronDownIcon, XIcon} from "@heroicons/react/outline"
|
||||||
|
import {useForm} from "react-hook-form"
|
||||||
|
|
||||||
|
import {QuestionWhereInput} from "../../__generated__/graphql-schema"
|
||||||
|
import {useDashboardContext, whereDefaultState} from "./DashboardContext"
|
||||||
|
import {useCurrentUser} from "../../contexts"
|
||||||
|
import {Button, Input} from "../../components"
|
||||||
|
|
||||||
|
type FilterBarForm = {
|
||||||
|
fromOtherUsers?: boolean
|
||||||
|
createDate: {
|
||||||
|
startAt: string
|
||||||
|
endAt: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDateISO8601Date = '2021-01-01'
|
||||||
|
const currentISO8601Date = new Date().toISOString().split('T')[0]
|
||||||
|
|
||||||
|
const formDefaultValues: FilterBarForm = {
|
||||||
|
fromOtherUsers: false,
|
||||||
|
createDate: {
|
||||||
|
startAt: startDateISO8601Date,
|
||||||
|
endAt: currentISO8601Date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapFilter = (values: FilterBarForm, userId?: string): QuestionWhereInput => ({
|
||||||
|
userId: values.fromOtherUsers ? undefined : userId,
|
||||||
|
createDate: {
|
||||||
|
startAt: values.createDate.startAt.length ? values.createDate.startAt : startDateISO8601Date,
|
||||||
|
endAt: values.createDate.endAt.length ? values.createDate.endAt : currentISO8601Date,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const FiltersForm: FC = () => {
|
||||||
|
const {register, handleSubmit, reset, getValues, formState} = useForm({
|
||||||
|
defaultValues: formDefaultValues,
|
||||||
|
})
|
||||||
|
const {setWhere} = useDashboardContext()
|
||||||
|
const userContext = useCurrentUser()
|
||||||
|
const {user, isOnlyTeacher} = userContext
|
||||||
|
|
||||||
|
const onSubmit = (values: FilterBarForm) => {
|
||||||
|
reset(getValues(), {
|
||||||
|
isDirty: false
|
||||||
|
})
|
||||||
|
setWhere(mapFilter(values, user?.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClean = () => {
|
||||||
|
reset(formDefaultValues)
|
||||||
|
setWhere(whereDefaultState(userContext))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className={"flex justify-between"}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<label className={"pl-2 pt-2"}>Data de Criação</label>
|
||||||
|
<div className={"grid grid-cols-2 gap-2 border p-2 m-2 rounded-md border-gray-300"}>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
placeholder="createDate.startAt"
|
||||||
|
ref={register({
|
||||||
|
maxLength: 10,
|
||||||
|
minLength: 10,
|
||||||
|
})}
|
||||||
|
name={"createDate.startAt"}
|
||||||
|
label={"A Partir De"}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
placeholder="createDate.endAt"
|
||||||
|
ref={register({
|
||||||
|
maxLength: 10,
|
||||||
|
minLength: 10,
|
||||||
|
})}
|
||||||
|
name={"createDate.endAt"}
|
||||||
|
label={"Até"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
{!isOnlyTeacher && (
|
||||||
|
<span className={"flex items-center"}>
|
||||||
|
<label
|
||||||
|
htmlFor={"fromOtherUsers"}
|
||||||
|
children={"Apenas questões próprias?"}
|
||||||
|
className={"mr-3"}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
id={"fromOtherUsers"}
|
||||||
|
type="checkbox"
|
||||||
|
placeholder="fromOtherUsers"
|
||||||
|
ref={register}
|
||||||
|
name={"fromOtherUsers"}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className={"grid grid-cols-2 gap-2 place-items-center"}>
|
||||||
|
<div>
|
||||||
|
<Button type={'tertiary'} onClick={handleClean}>
|
||||||
|
<span className={"flex"}>
|
||||||
|
<XIcon className={"w-5 h-5 text-gray-800"}/>
|
||||||
|
Limpar filtro
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button disabled={!formState.isDirty} type={'primary'} htmlType={"submit"} className={"w-full"}>
|
||||||
|
Filtar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Filters: FC = () => (
|
||||||
|
<Disclosure>
|
||||||
|
{({open}) => (
|
||||||
|
<div className="m-auto bg-white rounded-md shadow-sm hover:shadow transition-shadow duration-300">
|
||||||
|
<Disclosure.Button as={Fragment}>
|
||||||
|
<button className="flex p-2 w-full justify-between">
|
||||||
|
<div className="grid place-items-center pl-4">
|
||||||
|
Filtros
|
||||||
|
</div>
|
||||||
|
<div className={"pr-4"}>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={`${open ? 'transform rotate-180' : ''} w-5 h-5 text-gray-800`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
enter="transition duration-100 ease-out"
|
||||||
|
enterFrom="transform scale-95 opacity-0"
|
||||||
|
enterTo="transform scale-100 opacity-100"
|
||||||
|
leave="transition duration-75 ease-out"
|
||||||
|
leaveFrom="transform scale-100 opacity-100"
|
||||||
|
leaveTo="transform scale-95 opacity-0"
|
||||||
|
>
|
||||||
|
<Disclosure.Panel className={"p-4"}>
|
||||||
|
<FiltersForm/>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
)
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import React, {FC} from 'react'
|
||||||
|
import {gql, useQuery} from '@apollo/client'
|
||||||
|
|
||||||
|
import {Query} from '../../../__generated__/graphql-schema'
|
||||||
|
import {Pie} from '../components/charts'
|
||||||
|
import {useDashboardContext} from "../DashboardContext";
|
||||||
|
|
||||||
|
type ResponsivePieData = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
value: number
|
||||||
|
}[]
|
||||||
|
|
||||||
|
type QuestionsByBloomTaxonomyCountQuery = {
|
||||||
|
remember: Query['questions']
|
||||||
|
understand: Query['questions']
|
||||||
|
apply: Query['questions']
|
||||||
|
analyze: Query['questions']
|
||||||
|
evaluate: Query['questions']
|
||||||
|
create: Query['questions']
|
||||||
|
}
|
||||||
|
|
||||||
|
const QuestionsByBloomTaxonomyCount = gql`
|
||||||
|
query QuestionsByBloomTaxonomyCount (
|
||||||
|
$rememberWhere: QuestionWhereInput!,
|
||||||
|
$understandWhere: QuestionWhereInput!,
|
||||||
|
$applyWhere: QuestionWhereInput!,
|
||||||
|
$analyzeWhere: QuestionWhereInput!,
|
||||||
|
$evaluateWhere: QuestionWhereInput!,
|
||||||
|
$createWhere: QuestionWhereInput!,
|
||||||
|
) {
|
||||||
|
remember: questions(where: $rememberWhere) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
understand: questions(where: $understandWhere) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
apply: questions(where: $applyWhere) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
analyze: questions(where: $analyzeWhere) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
evaluate: questions(where: $evaluateWhere) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
create: questions(where: $createWhere) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const QuestionByBloomTaxonomy: FC = () => {
|
||||||
|
const {where} = useDashboardContext()
|
||||||
|
const {loading, data} = useQuery<QuestionsByBloomTaxonomyCountQuery>(
|
||||||
|
QuestionsByBloomTaxonomyCount, {
|
||||||
|
variables: {
|
||||||
|
rememberWhere: {bloomTaxonomy: ['remember'], ...where},
|
||||||
|
understandWhere: {bloomTaxonomy: ['understand'], ...where},
|
||||||
|
applyWhere: {bloomTaxonomy: ['apply'], ...where},
|
||||||
|
analyzeWhere: {bloomTaxonomy: ['analyze'], ...where},
|
||||||
|
evaluateWhere: {bloomTaxonomy: ['evaluate'], ...where},
|
||||||
|
createWhere: {bloomTaxonomy: ['create'], ...where},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loading || !data) return null
|
||||||
|
|
||||||
|
const mappedData: ResponsivePieData = [
|
||||||
|
{
|
||||||
|
id: "Recordar",
|
||||||
|
label: "Recordar",
|
||||||
|
value: data.remember.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Compreender",
|
||||||
|
label: "Compreender",
|
||||||
|
value: data.understand.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Aplicar",
|
||||||
|
label: "Aplicar",
|
||||||
|
value: data.apply.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Analisar",
|
||||||
|
label: "Analisar",
|
||||||
|
value: data.analyze.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Avaliar",
|
||||||
|
label: "Avaliar",
|
||||||
|
value: data.evaluate.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Criar",
|
||||||
|
label: "Criar",
|
||||||
|
value: data.create.totalCount ?? 0
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const filteredData = mappedData.filter(item => item.value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pie title="Questões por Habilidade Cognitiva" data={filteredData}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
150
app/javascript/pages/dashboard/charts/QuestionsByCheckType.tsx
Normal file
150
app/javascript/pages/dashboard/charts/QuestionsByCheckType.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import React, {FC} from 'react'
|
||||||
|
import {gql, useQuery} from '@apollo/client'
|
||||||
|
|
||||||
|
import {Query} from '../../../__generated__/graphql-schema'
|
||||||
|
import {Pie} from '../components/charts'
|
||||||
|
import {useDashboardContext} from "../DashboardContext";
|
||||||
|
|
||||||
|
type ResponsivePieData = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
value: number
|
||||||
|
}[]
|
||||||
|
|
||||||
|
type QuestionsByCheckTypeCountQuery = {
|
||||||
|
uniqueAnswer: Query['questions']
|
||||||
|
incompleteAffirmation: Query['questions']
|
||||||
|
multipleAnswer: Query['questions']
|
||||||
|
negativeFocus: Query['questions']
|
||||||
|
assertionAndReason: Query['questions']
|
||||||
|
gap: Query['questions']
|
||||||
|
interpretation: Query['questions']
|
||||||
|
association: Query['questions']
|
||||||
|
orderingOrRanking: Query['questions']
|
||||||
|
constantAlternatives: Query['questions']
|
||||||
|
}
|
||||||
|
|
||||||
|
const QuestionsByCheckTypeCount = gql`
|
||||||
|
query QuestionsByCheckTypeCount(
|
||||||
|
$uniqueAnswer: QuestionWhereInput!,
|
||||||
|
$incompleteAffirmation: QuestionWhereInput!,
|
||||||
|
$multipleAnswer: QuestionWhereInput!,
|
||||||
|
$negativeFocus: QuestionWhereInput!,
|
||||||
|
$assertionAndReason: QuestionWhereInput!,
|
||||||
|
$gap: QuestionWhereInput!,
|
||||||
|
$interpretation: QuestionWhereInput!,
|
||||||
|
$association: QuestionWhereInput!,
|
||||||
|
$orderingOrRanking: QuestionWhereInput!,
|
||||||
|
$constantAlternatives: QuestionWhereInput!,
|
||||||
|
) {
|
||||||
|
uniqueAnswer: questions(where: $uniqueAnswer) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
incompleteAffirmation: questions(where: $incompleteAffirmation) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
multipleAnswer: questions(where: $multipleAnswer) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
negativeFocus: questions(where: $negativeFocus) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
assertionAndReason: questions(where: $assertionAndReason) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
gap: questions(where: $gap) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
interpretation: questions(where: $interpretation) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
association: questions(where: $association) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
orderingOrRanking: questions(where: $orderingOrRanking) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
constantAlternatives: questions(where: $constantAlternatives) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const QuestionByCheckType: FC = () => {
|
||||||
|
const {where} = useDashboardContext()
|
||||||
|
const {loading, data} = useQuery<QuestionsByCheckTypeCountQuery>(
|
||||||
|
QuestionsByCheckTypeCount, {
|
||||||
|
variables: {
|
||||||
|
uniqueAnswer: {checkType: ['unique_answer'], ...where},
|
||||||
|
incompleteAffirmation: {checkType: ['incomplete_affirmation'], ...where},
|
||||||
|
multipleAnswer: {checkType: ['multiple_answer'], ...where},
|
||||||
|
negativeFocus: {checkType: ['negative_focus'], ...where},
|
||||||
|
assertionAndReason: {checkType: ['assertion_and_reason'], ...where},
|
||||||
|
gap: {checkType: ['gap'], ...where},
|
||||||
|
interpretation: {checkType: ['interpretation'], ...where},
|
||||||
|
association: {checkType: ['association'], ...where},
|
||||||
|
orderingOrRanking: {checkType: ['ordering_or_ranking'], ...where},
|
||||||
|
constantAlternatives: {checkType: ['constant_alternatives'], ...where},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loading || !data) return null
|
||||||
|
|
||||||
|
const mappedData: ResponsivePieData = [
|
||||||
|
{
|
||||||
|
id: "Asserção e Razão",
|
||||||
|
label: "Asserção e Razão",
|
||||||
|
value: data.assertionAndReason.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Associação",
|
||||||
|
label: "Associação",
|
||||||
|
value: data.association.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Alternativas Constantes",
|
||||||
|
label: "Alternativas Constantes",
|
||||||
|
value: data.constantAlternatives.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lacuna",
|
||||||
|
label: "Lacuna",
|
||||||
|
value: data.gap.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Afirmação Incompleta",
|
||||||
|
label: "Afirmação Incompleta",
|
||||||
|
value: data.incompleteAffirmation.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Interpretação",
|
||||||
|
label: "Interpretação",
|
||||||
|
value: data.interpretation.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Resposta Múltipla",
|
||||||
|
label: "Resposta Múltipla",
|
||||||
|
value: data.multipleAnswer.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Foco Negativo",
|
||||||
|
label: "Foco Negativo",
|
||||||
|
value: data.negativeFocus.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ordenação ou Seriação",
|
||||||
|
label: "Ordenação ou Seriação",
|
||||||
|
value: data.orderingOrRanking.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Resposta Única",
|
||||||
|
label: "Resposta Única",
|
||||||
|
value: data.uniqueAnswer.totalCount ?? 0
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const filteredData = mappedData.filter(item => item.value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pie title="Questões por Tipo" data={filteredData}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import React, {FC} from 'react'
|
||||||
|
import {gql, useQuery} from '@apollo/client'
|
||||||
|
|
||||||
|
import {Query} from '../../../__generated__/graphql-schema'
|
||||||
|
import {Pie} from '../components/charts'
|
||||||
|
import {useDashboardContext} from "../DashboardContext";
|
||||||
|
|
||||||
|
type ResponsivePieData = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
value: number
|
||||||
|
}[]
|
||||||
|
|
||||||
|
type QuestionsByDifficultyCountQuery = {
|
||||||
|
easy: Query['questions']
|
||||||
|
medium: Query['questions']
|
||||||
|
hard: Query['questions']
|
||||||
|
}
|
||||||
|
|
||||||
|
const QuestionsByDifficultyCount = gql`
|
||||||
|
query QuestionsByDifficultyCount(
|
||||||
|
$easyWhere: QuestionWhereInput!,
|
||||||
|
$mediumWhere: QuestionWhereInput!,
|
||||||
|
$hardWhere: QuestionWhereInput!,
|
||||||
|
) {
|
||||||
|
easy: questions(where: $easyWhere) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
medium: questions(where: $mediumWhere) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
hard: questions(where: $hardWhere) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const QuestionsByDifficulty: FC = () => {
|
||||||
|
const {where} = useDashboardContext()
|
||||||
|
const {loading, data} = useQuery<QuestionsByDifficultyCountQuery>(
|
||||||
|
QuestionsByDifficultyCount, {
|
||||||
|
variables: {
|
||||||
|
easyWhere: {difficulty: ['easy'], ...where},
|
||||||
|
mediumWhere: {difficulty: ['medium'], ...where},
|
||||||
|
hardWhere: {difficulty: ['hard'], ...where},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loading || !data) return null
|
||||||
|
|
||||||
|
const mappedData: ResponsivePieData = [
|
||||||
|
{
|
||||||
|
id: "Fácil",
|
||||||
|
label: "Fácil",
|
||||||
|
value: data.easy.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Difícil",
|
||||||
|
label: "Difícil",
|
||||||
|
value: data.hard.totalCount ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Média",
|
||||||
|
label: "Média",
|
||||||
|
value: data.medium.totalCount ?? 0
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const filteredData = mappedData.filter(item => item.value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pie title="Questões por Dificuldade" data={filteredData}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
50
app/javascript/pages/dashboard/charts/QuestionsBySubject.tsx
Normal file
50
app/javascript/pages/dashboard/charts/QuestionsBySubject.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React, {FC} from 'react'
|
||||||
|
import {gql, useQuery} from '@apollo/client'
|
||||||
|
|
||||||
|
import {Query} from '../../../__generated__/graphql-schema'
|
||||||
|
import {Pie} from '../components/charts'
|
||||||
|
import {useDashboardContext} from "../DashboardContext";
|
||||||
|
|
||||||
|
type ResponsivePieData = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
value: number
|
||||||
|
}[]
|
||||||
|
|
||||||
|
const QuestionsBySubjectCount = gql`
|
||||||
|
query QuestionsBySubjectCount($where: QuestionWhereInput!) {
|
||||||
|
subjects {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
questions(where: $where) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const QuestionsBySubject: FC = () => {
|
||||||
|
const {where} = useDashboardContext()
|
||||||
|
const {loading, data} = useQuery<Query>(QuestionsBySubjectCount, {
|
||||||
|
variables: {
|
||||||
|
where,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loading) return null
|
||||||
|
|
||||||
|
const subjects = data?.subjects.nodes ?? []
|
||||||
|
const subjectWithQuestions = subjects.filter(subject => !!subject?.questions.totalCount)
|
||||||
|
const mappedData: ResponsivePieData = subjectWithQuestions.map(subject => ({
|
||||||
|
id: subject.name,
|
||||||
|
label: subject.name,
|
||||||
|
value: subject.questions.totalCount,
|
||||||
|
}))
|
||||||
|
const filteredData = mappedData.filter(item => item.value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pie title="Questões por Assunto" data={filteredData}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
4
app/javascript/pages/dashboard/charts/index.ts
Normal file
4
app/javascript/pages/dashboard/charts/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./QuestionsBySubject";
|
||||||
|
export * from "./QuestionsByBloomTaxonomy";
|
||||||
|
export * from "./QuestionsByDifficulty";
|
||||||
|
export * from "./QuestionsByCheckType";
|
||||||
38
app/javascript/pages/dashboard/components/charts/Pie.tsx
Normal file
38
app/javascript/pages/dashboard/components/charts/Pie.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {ResponsivePie} from '@nivo/pie'
|
||||||
|
import React, {FC} from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
data: {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
value: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Pie: FC<Props> = ({title, data}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="m-auto bg-white rounded-md p-4 shadow-sm hover:shadow transition-shadow duration-300"
|
||||||
|
style={{ height: '36rem', width: '36rem' }}
|
||||||
|
>
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">{title}</h3>
|
||||||
|
<ResponsivePie
|
||||||
|
data={data}
|
||||||
|
margin={{top: 40, right: 80, bottom: 80, left: 80}}
|
||||||
|
innerRadius={0.5}
|
||||||
|
padAngle={0.7}
|
||||||
|
cornerRadius={3}
|
||||||
|
activeOuterRadiusOffset={8}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor={{from: 'color', modifiers: [['darker', 0.2]]}}
|
||||||
|
arcLinkLabelsSkipAngle={10}
|
||||||
|
arcLinkLabelsTextColor="#333333"
|
||||||
|
arcLinkLabelsThickness={2}
|
||||||
|
arcLinkLabelsColor={{from: 'color'}}
|
||||||
|
arcLabelsSkipAngle={10}
|
||||||
|
arcLabelsTextColor={{from: 'color', modifiers: [['darker', 2]]}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Pie";
|
||||||
1
app/javascript/pages/dashboard/components/index.ts
Normal file
1
app/javascript/pages/dashboard/components/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./charts";
|
||||||
1
app/javascript/pages/dashboard/index.ts
Normal file
1
app/javascript/pages/dashboard/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Dashboard";
|
||||||
127
app/javascript/pages/question/Edit/Edit.tsx
Normal file
127
app/javascript/pages/question/Edit/Edit.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import React, {FC, useState} from 'react'
|
||||||
|
import {useHistory, useParams} from 'react-router';
|
||||||
|
import {gql, useMutation, useQuery} from '@apollo/client';
|
||||||
|
|
||||||
|
import {Mutation, Query, Question} from '../../../__generated__/graphql-schema';
|
||||||
|
import {AlertV2Props, Navigator} from '../../../components';
|
||||||
|
import {Form, FormFragments} from '../Form'
|
||||||
|
import {NodeId} from '../../../utils/graphql';
|
||||||
|
import {QuestionRoutePaths} from "../../../routes";
|
||||||
|
|
||||||
|
const GET_QUESTION = gql`
|
||||||
|
${FormFragments}
|
||||||
|
query Question ($id: ID!) {
|
||||||
|
node (id: $id) {
|
||||||
|
__typename
|
||||||
|
...on Question {
|
||||||
|
id
|
||||||
|
...FormFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UPDATE_QUESTION_MUTATOIN = gql`
|
||||||
|
mutation($input: UpdateQuestionInput!) {
|
||||||
|
updateQuestion(input: $input) {
|
||||||
|
question {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Edit: FC = () => {
|
||||||
|
const history = useHistory()
|
||||||
|
const [alert, setAlert] = useState<AlertV2Props>()
|
||||||
|
const params = useParams<{ id: string }>()
|
||||||
|
const [updateQuestion] = useMutation<Mutation>(UPDATE_QUESTION_MUTATOIN)
|
||||||
|
const {loading, data} = useQuery<Query>(
|
||||||
|
GET_QUESTION, {
|
||||||
|
variables: {
|
||||||
|
id: params.id,
|
||||||
|
fetchPolicy: "no-cache"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const question = data?.node as Question | null
|
||||||
|
|
||||||
|
if (loading || !question) return null
|
||||||
|
|
||||||
|
const recordId = NodeId.decode(question.id).id
|
||||||
|
|
||||||
|
const onSubmit = (inputs: any) => {
|
||||||
|
updateQuestion({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
question: {
|
||||||
|
...inputs,
|
||||||
|
id: recordId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
history.push(QuestionRoutePaths.index)
|
||||||
|
}).catch((error: string) => {
|
||||||
|
setAlert({
|
||||||
|
severity: "error",
|
||||||
|
text: `Erro ao atualizar questão. ${error}. Por favor, tente novamente.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
() => setAlert({severity: "error", text: ""}),
|
||||||
|
3000
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDraftSubmit = (inputs: any) => {
|
||||||
|
updateQuestion({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
question: {
|
||||||
|
...inputs,
|
||||||
|
id: recordId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
setAlert({
|
||||||
|
severity: "success",
|
||||||
|
text: "Rascunho atualizado com sucesso",
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setAlert(undefined)
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
}).catch((error: string) => {
|
||||||
|
setAlert({
|
||||||
|
severity: "error",
|
||||||
|
text: `Erro ao atualizar rascunho. ${error}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
() => setAlert(undefined),
|
||||||
|
8000
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navigator home/>
|
||||||
|
<div className="bg-gray-100 w-full my-2">
|
||||||
|
<main>
|
||||||
|
<Form
|
||||||
|
question={question}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onDraftSubmit={onDraftSubmit}
|
||||||
|
alert={alert}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
app/javascript/pages/question/Edit/index.ts
Normal file
1
app/javascript/pages/question/Edit/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Edit";
|
||||||
222
app/javascript/pages/question/Form/Form.tsx
Normal file
222
app/javascript/pages/question/Form/Form.tsx
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import React, {FC, useState} from 'react'
|
||||||
|
import {useForm} from 'react-hook-form';
|
||||||
|
import {ExclamationCircleIcon} from '@heroicons/react/outline';
|
||||||
|
import {useHistory} from "react-router-dom";
|
||||||
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
|
import {gql} from '@apollo/client';
|
||||||
|
|
||||||
|
import {Question, QuestionCreateInput, QuestionStatus} from '../../../__generated__/graphql-schema';
|
||||||
|
import {formatInput} from '../formatInputs';
|
||||||
|
import {validateQuestionInputs} from '../../../utils/questions/questionValidations';
|
||||||
|
import {RootState} from '../../../services/store';
|
||||||
|
import {FormProvider} from './FormContext'
|
||||||
|
import {SteppedForm, Step} from './SteppedForm'
|
||||||
|
import {
|
||||||
|
EnunciationFormStep,
|
||||||
|
EnunciationFragment,
|
||||||
|
AnswerFormStep,
|
||||||
|
AnswerFragment,
|
||||||
|
DistractorsFormStep,
|
||||||
|
DistractorsFragment,
|
||||||
|
FeaturesFormStep,
|
||||||
|
FeaturesFragment,
|
||||||
|
} from './steps'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
AlertV2Props,
|
||||||
|
AlertV2,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
} from '../../../components';
|
||||||
|
import {QuestionRoutePaths} from "../../../routes";
|
||||||
|
import {turnOff, turnOn} from "../../../services/store/unsavedChanges";
|
||||||
|
|
||||||
|
export const FormFragments = gql`
|
||||||
|
${EnunciationFragment}
|
||||||
|
${AnswerFragment}
|
||||||
|
${DistractorsFragment}
|
||||||
|
${FeaturesFragment}
|
||||||
|
fragment FormFields on Question {
|
||||||
|
...EnunciationFields
|
||||||
|
...AnswerFields
|
||||||
|
...DistractorsFields
|
||||||
|
...FeaturesFields
|
||||||
|
status
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
question?: Question
|
||||||
|
onSubmit?: (inputs: any) => void
|
||||||
|
onDraftSubmit?: (inputs: any) => void
|
||||||
|
alert?: AlertV2Props
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Form: FC<Props> = ({question, onSubmit, onDraftSubmit, alert}) => {
|
||||||
|
const [validationErrors, setValidationErrors] = useState<string[]>([])
|
||||||
|
const [confirmSaveDialogIsOpen, setConfirmFinishDialogIsOpen] = useState(false)
|
||||||
|
const [leaveDialogIsOpen, setLeaveDialogIsOpen] = useState(false)
|
||||||
|
const {register, control, setValue, getValues, reset, formState} = useForm()
|
||||||
|
const [currentStep, setCurrentStep] = useState(0)
|
||||||
|
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
|
||||||
|
const history = useHistory()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const minStep = 0
|
||||||
|
const maxStep = 3
|
||||||
|
const onFirstStep = currentStep === minStep
|
||||||
|
const onLastStep = currentStep === maxStep
|
||||||
|
|
||||||
|
if (formState.isDirty) {
|
||||||
|
dispatch(turnOn())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNextStep = () => {
|
||||||
|
if (onLastStep) return
|
||||||
|
|
||||||
|
setCurrentStep(currentStep + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePreviousStep = () => {
|
||||||
|
if (onFirstStep) return
|
||||||
|
|
||||||
|
setCurrentStep(currentStep - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFormattedInputValues = () => formatInput(getValues())
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (unsavedChanges && !leaveDialogIsOpen) {
|
||||||
|
setLeaveDialogIsOpen(true)
|
||||||
|
} else {
|
||||||
|
history.push(QuestionRoutePaths.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDraftSave = () => {
|
||||||
|
if (onDraftSubmit) {
|
||||||
|
onDraftSubmit({...getFormattedInputValues(), status: QuestionStatus.Draft} as QuestionCreateInput)
|
||||||
|
reset(getValues(), {
|
||||||
|
isDirty: false
|
||||||
|
})
|
||||||
|
dispatch(turnOff())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const inputs = {...getFormattedInputValues(), status: QuestionStatus.WaitingReview} as QuestionCreateInput
|
||||||
|
const errors = validateQuestionInputs(inputs)
|
||||||
|
|
||||||
|
setConfirmFinishDialogIsOpen(false)
|
||||||
|
|
||||||
|
if (onSubmit && !errors.length) {
|
||||||
|
dispatch(turnOff())
|
||||||
|
onSubmit(inputs)
|
||||||
|
} else {
|
||||||
|
setValidationErrors(errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(getValues(), {
|
||||||
|
isDirty: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider props={{question, hooks: {register, control, setValue}}}>
|
||||||
|
{alert && (
|
||||||
|
<AlertV2 severity={alert.severity} text={alert.text}></AlertV2>
|
||||||
|
)}
|
||||||
|
<Dialog
|
||||||
|
isOpen={leaveDialogIsOpen}
|
||||||
|
setIsOpen={setLeaveDialogIsOpen}
|
||||||
|
onConfirmation={handleCancel}
|
||||||
|
title="Modificações não Salvas"
|
||||||
|
text="Todas as alterações serão descartadas. Deseja continuar?"
|
||||||
|
/>
|
||||||
|
<Dialog
|
||||||
|
isOpen={confirmSaveDialogIsOpen}
|
||||||
|
setIsOpen={setConfirmFinishDialogIsOpen}
|
||||||
|
onConfirmation={handleSave}
|
||||||
|
title="Modificações não Salvas"
|
||||||
|
text="Ao finalizar a questão, o revisor receberá uma notificação para revisá-la. Deseja continuar?"
|
||||||
|
/>
|
||||||
|
<Dialog
|
||||||
|
isOpen={!!validationErrors.length}
|
||||||
|
setIsOpen={() => setValidationErrors([])}
|
||||||
|
onConfirmation={() => setValidationErrors([])}
|
||||||
|
title="Falha de Validação"
|
||||||
|
type="notice"
|
||||||
|
text={
|
||||||
|
<>
|
||||||
|
<List>
|
||||||
|
{validationErrors?.map((errorMessage) => (
|
||||||
|
<ListItem
|
||||||
|
key={errorMessage}
|
||||||
|
icon={<ExclamationCircleIcon className="w-5 text-gray-800"/>}
|
||||||
|
text={errorMessage}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<form className="m-auto max-w-screen-md">
|
||||||
|
<SteppedForm
|
||||||
|
currentStep={currentStep}
|
||||||
|
className="mb-3"
|
||||||
|
>
|
||||||
|
<Step step={0}>
|
||||||
|
<EnunciationFormStep/>
|
||||||
|
</Step>
|
||||||
|
<Step step={1}>
|
||||||
|
<AnswerFormStep/>
|
||||||
|
</Step>
|
||||||
|
<Step step={2}>
|
||||||
|
<DistractorsFormStep/>
|
||||||
|
</Step>
|
||||||
|
<Step step={3}>
|
||||||
|
<FeaturesFormStep/>
|
||||||
|
</Step>
|
||||||
|
</SteppedForm>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="mx-3 sm:mx-0 flex justify-items-center flex-col-reverse sm:flex-row justify-end space-x-0 sm:space-x-2">
|
||||||
|
<Button
|
||||||
|
className={"mb-3 sm:mb-0"}
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={`mb-3 sm:mb-0 ${onFirstStep ? "hidden" : ""}`}
|
||||||
|
onClick={handlePreviousStep}
|
||||||
|
>
|
||||||
|
Retornar
|
||||||
|
</Button>
|
||||||
|
{(question?.status === QuestionStatus.Draft || question?.status === undefined) &&
|
||||||
|
<Button className={"mb-3 sm:mb-0"} onClick={handleDraftSave}>
|
||||||
|
Salvar Rascunho
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className={`mb-3 sm:mb-0 ${onLastStep ? "hidden" : ""}`}
|
||||||
|
onClick={handleNextStep}
|
||||||
|
>
|
||||||
|
Prosseguir
|
||||||
|
</Button>
|
||||||
|
{onLastStep &&
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className="mb-3 sm:mb-0"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
Finalizar
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
39
app/javascript/pages/question/Form/FormContext.tsx
Normal file
39
app/javascript/pages/question/Form/FormContext.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { FC, useContext } from 'react'
|
||||||
|
import { Control, FieldValues } from 'react-hook-form';
|
||||||
|
import { Question } from '../../../__generated__/graphql-schema';
|
||||||
|
|
||||||
|
type FormContextHooks = {
|
||||||
|
register: any
|
||||||
|
setValue: Function
|
||||||
|
control: Control<FieldValues>
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormContextProps = {
|
||||||
|
hooks: FormContextHooks
|
||||||
|
question?: Question
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormContext = React.createContext<FormContextProps | null>(null);
|
||||||
|
|
||||||
|
export const useFormProvider = () => {
|
||||||
|
const context = useContext(FormContext)
|
||||||
|
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error('You probably forgot to put <FormProvider>.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children?: any
|
||||||
|
props: FormContextProps
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormProvider: FC<Props> = ({ children, props }) => {
|
||||||
|
return (
|
||||||
|
<FormContext.Provider value={props}>
|
||||||
|
{children}
|
||||||
|
</FormContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
34
app/javascript/pages/question/Form/SteppedForm.tsx
Normal file
34
app/javascript/pages/question/Form/SteppedForm.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
|
||||||
|
type StepProps = {
|
||||||
|
children: any
|
||||||
|
step: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Step: FC<StepProps> = ({ children }) => (children);
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: any;
|
||||||
|
currentStep: number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SteppedForm: FC<Props> = ({
|
||||||
|
children,
|
||||||
|
currentStep,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
{children?.map((x: any) => {
|
||||||
|
const visible = x.props.step === currentStep;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={x.props.step} className={visible ? "" : "hidden"}>
|
||||||
|
{x}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
55
app/javascript/pages/question/Form/components/TextEditor.tsx
Normal file
55
app/javascript/pages/question/Form/components/TextEditor.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import CKEditor from "@ckeditor/ckeditor5-react";
|
||||||
|
import * as ClassicEditor from "ckeditor5-mathtype/build/ckeditor";
|
||||||
|
|
||||||
|
import { useFormProvider } from '../FormContext'
|
||||||
|
|
||||||
|
const toolbarOptions = [
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"blockQuote",
|
||||||
|
"numberedList",
|
||||||
|
"bulletedList",
|
||||||
|
"imageUpload",
|
||||||
|
"insertTable",
|
||||||
|
"tableColumn",
|
||||||
|
"tableRow",
|
||||||
|
"mergeTableCells",
|
||||||
|
"|",
|
||||||
|
"MathType",
|
||||||
|
"ChemType",
|
||||||
|
"|",
|
||||||
|
"undo",
|
||||||
|
"redo",
|
||||||
|
];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string
|
||||||
|
defaultValue: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextEditor: FC<Props> = ({ name, defaultValue }) => {
|
||||||
|
const { hooks: { control } } = useFormProvider()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<CKEditor
|
||||||
|
editor={ClassicEditor}
|
||||||
|
data={value}
|
||||||
|
config={{
|
||||||
|
toolbar: toolbarOptions,
|
||||||
|
ckfinder: {
|
||||||
|
uploadUrl: `${process.env.REACT_APP_BACKEND_URL}/uploads`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onChange={(_: any, editor: any) => onChange(editor.getData())}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
app/javascript/pages/question/Form/index.ts
Normal file
1
app/javascript/pages/question/Form/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Form";
|
||||||
61
app/javascript/pages/question/Form/steps/AnswerFormStep.tsx
Normal file
61
app/javascript/pages/question/Form/steps/AnswerFormStep.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { gql } from "@apollo/client";
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Card } from "../../../../components/Card/Card";
|
||||||
|
import { TextEditor } from "../components/TextEditor";
|
||||||
|
import { useFormProvider } from '../FormContext'
|
||||||
|
|
||||||
|
export const AnswerFragment = gql`
|
||||||
|
fragment AnswerFields on Question {
|
||||||
|
alternatives {
|
||||||
|
correct
|
||||||
|
text
|
||||||
|
}
|
||||||
|
explanation
|
||||||
|
references
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const AnswerFormStep: FC = () => {
|
||||||
|
const { question, hooks: { control } } = useFormProvider()
|
||||||
|
|
||||||
|
const alternativesMaped = question?.alternatives || [
|
||||||
|
{ text: "", correct: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const correctAlternative = alternativesMaped.find(
|
||||||
|
(alternative) => alternative.correct === true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card title="Resposta Correta" className="mb-3">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="w-full">
|
||||||
|
<TextEditor
|
||||||
|
name={`alternatives[0].text`}
|
||||||
|
defaultValue={correctAlternative?.text ?? ''}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name={`alternatives[0].correct`}
|
||||||
|
control={control}
|
||||||
|
defaultValue={true}
|
||||||
|
render={() => (<></>)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-full border border-gray-300 rounded p-4 mt-4 shadow-sm">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-medium">Explicação</h2>
|
||||||
|
<TextEditor name="explanation" defaultValue={question?.explanation ?? ''} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-medium">Referências</h2>
|
||||||
|
<TextEditor defaultValue={question?.references ?? ''} name="references" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
|
import { Card } from "../../../../components";
|
||||||
|
import { TextEditor } from "../components/TextEditor";
|
||||||
|
import { useFormProvider } from '../FormContext'
|
||||||
|
|
||||||
|
export const DistractorsFragment = gql`
|
||||||
|
fragment DistractorsFields on Question {
|
||||||
|
alternatives {
|
||||||
|
correct
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const DistractorsFormStep: FC = () => {
|
||||||
|
const { question, hooks: { control } } = useFormProvider()
|
||||||
|
|
||||||
|
const incorrectAnswers = question?.alternatives?.filter(
|
||||||
|
(alternative) => alternative.correct === false,
|
||||||
|
) || [
|
||||||
|
{ text: "", correct: false },
|
||||||
|
{ text: "", correct: false },
|
||||||
|
{ text: "", correct: false },
|
||||||
|
{ text: "", correct: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card title="Distratores">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="">
|
||||||
|
{incorrectAnswers.map(({ text }, index) => (
|
||||||
|
<div className="w-full mb-3" key={index}>
|
||||||
|
<TextEditor
|
||||||
|
name={`alternatives[${index + 1}].text`}
|
||||||
|
defaultValue={text ?? ""}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name={`alternatives[${index + 1}].correct`}
|
||||||
|
control={control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={() => (<></>)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { gql } from "@apollo/client";
|
||||||
|
import React, { FC } from "react";
|
||||||
|
|
||||||
|
import { Card } from "../../../../components";
|
||||||
|
import { TextEditor } from '../components/TextEditor'
|
||||||
|
import { useFormProvider } from '../FormContext'
|
||||||
|
|
||||||
|
export const EnunciationFragment = gql`
|
||||||
|
fragment EnunciationFields on Question {
|
||||||
|
instruction
|
||||||
|
support
|
||||||
|
body
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const EnunciationFormStep: FC = () => {
|
||||||
|
const { question } = useFormProvider()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card className="h-full mb-3" title="Instrução (opcional)">
|
||||||
|
<TextEditor name="instruction" defaultValue={question?.instruction ?? ""} />
|
||||||
|
</Card>
|
||||||
|
<Card className="h-full mb-3" title="Suporte (opcional)">
|
||||||
|
<TextEditor name="support" defaultValue={question?.support ?? ""} />
|
||||||
|
</Card>
|
||||||
|
<Card className="h-full mb-3" title="Enunciado">
|
||||||
|
<TextEditor name="body" defaultValue={question?.body ?? ""} />
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import React, { FC, useState } from "react";
|
||||||
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
|
import { Card } from "../../../../../components";
|
||||||
|
import { SubjectSelect, SubjectFragment } from "./SubjectSelect";
|
||||||
|
import { ReviewerSelect, ReviewerFragment } from "./ReviewSelect";
|
||||||
|
import { useFormProvider } from '../../FormContext'
|
||||||
|
|
||||||
|
import { BLOOM_TAXONOMY, CHECK_TYPE, DIFFICULTY } from "../../../../../utils/types";
|
||||||
|
import { Question } from "../../../../../__generated__/graphql-schema";
|
||||||
|
|
||||||
|
export const FeaturesFragment = gql`
|
||||||
|
${ReviewerFragment}
|
||||||
|
${SubjectFragment}
|
||||||
|
fragment FeaturesFields on Question {
|
||||||
|
... ReviewerFields
|
||||||
|
... SubjectFields
|
||||||
|
authorship
|
||||||
|
authorshipYear
|
||||||
|
difficulty
|
||||||
|
checkType
|
||||||
|
intention
|
||||||
|
bloomTaxonomy
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const FeaturesFormStep: FC = () => {
|
||||||
|
const { question, hooks: { setValue, register } } = useFormProvider();
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
const {
|
||||||
|
authorship,
|
||||||
|
authorshipYear,
|
||||||
|
difficulty,
|
||||||
|
bloomTaxonomy,
|
||||||
|
checkType,
|
||||||
|
} = question || {} as Question
|
||||||
|
|
||||||
|
const [ownQuestion, setOwnQuestion] = useState<boolean>(authorship === "UNIFESO" || authorship === undefined || authorship === null);
|
||||||
|
|
||||||
|
const handleOwnCheck = (value: string) => {
|
||||||
|
if (value === 'UNIFESO') {
|
||||||
|
setOwnQuestion(true)
|
||||||
|
setValue("authorship", "UNIFESO");
|
||||||
|
setValue("authorshipYear", currentYear.toString());
|
||||||
|
} else {
|
||||||
|
setOwnQuestion(false)
|
||||||
|
setValue("authorship", "");
|
||||||
|
setValue("authorshipYear", "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card title="Características">
|
||||||
|
<div className="grid grid-cols-2 col-gap-2">
|
||||||
|
<div className="flex">
|
||||||
|
<label htmlFor="own" className="mr-3 my-auto">
|
||||||
|
Autoria
|
||||||
|
</label>
|
||||||
|
<div className="my-auto">
|
||||||
|
<input
|
||||||
|
className="my-auto"
|
||||||
|
type="radio"
|
||||||
|
id="authorship-own"
|
||||||
|
checked={!!ownQuestion}
|
||||||
|
ref={register}
|
||||||
|
onChange={() => handleOwnCheck("UNIFESO")}
|
||||||
|
name="__nonused"
|
||||||
|
/>
|
||||||
|
<label htmlFor="authorship-own" className="ml-1">Própria</label>
|
||||||
|
</div>
|
||||||
|
<div className="my-auto ml-3">
|
||||||
|
<input
|
||||||
|
className="my-auto"
|
||||||
|
type="radio"
|
||||||
|
id="authorship-third"
|
||||||
|
checked={!ownQuestion}
|
||||||
|
ref={register}
|
||||||
|
onChange={() => handleOwnCheck("")}
|
||||||
|
name="__nonused"
|
||||||
|
/>
|
||||||
|
<label htmlFor="authorship-third" className="ml-1">Outro</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex">
|
||||||
|
<h2 className="pr-2 my-auto">Fonte</h2>
|
||||||
|
<div className="w-full">
|
||||||
|
<div style={{ maxWidth: "194px" }}>
|
||||||
|
<input
|
||||||
|
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
|
||||||
|
ref={register}
|
||||||
|
name="authorship"
|
||||||
|
defaultValue={authorship || (ownQuestion ? "UNIFESO" : "")}
|
||||||
|
readOnly={!!ownQuestion}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<h2 className="pr-2 pl-3 my-auto">Ano</h2>
|
||||||
|
<div style={{ maxWidth: "62px" }}>
|
||||||
|
<input
|
||||||
|
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||||
|
ref={register}
|
||||||
|
type="number"
|
||||||
|
min="1999"
|
||||||
|
max={currentYear}
|
||||||
|
step="1"
|
||||||
|
name="authorshipYear"
|
||||||
|
defaultValue={authorshipYear ?? new Date().getFullYear().toString()}
|
||||||
|
readOnly={!!ownQuestion}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 col-gap-2 mt-3">
|
||||||
|
<div className="w-full grid grid-cols-1 row-gap-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h2>Grau de Dificuldade</h2>
|
||||||
|
<select
|
||||||
|
ref={register}
|
||||||
|
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||||
|
name="difficulty"
|
||||||
|
defaultValue={difficulty ?? ""}
|
||||||
|
>
|
||||||
|
<option />
|
||||||
|
{DIFFICULTY.map((item, index) => (
|
||||||
|
<option key={index} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<h2>Tipo</h2>
|
||||||
|
<select
|
||||||
|
ref={register}
|
||||||
|
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||||
|
name="checkType"
|
||||||
|
defaultValue={checkType ?? ""}
|
||||||
|
>
|
||||||
|
<option />
|
||||||
|
{CHECK_TYPE.map((item, index) => (
|
||||||
|
<option key={index} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<h2>Habilidade Cognitiva</h2>
|
||||||
|
<select
|
||||||
|
ref={register}
|
||||||
|
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||||
|
name="bloomTaxonomy"
|
||||||
|
defaultValue={bloomTaxonomy ?? ""}
|
||||||
|
>
|
||||||
|
<option />
|
||||||
|
{BLOOM_TAXONOMY.map((item, index) => (
|
||||||
|
<option key={index} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<SubjectSelect />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col mt-4">
|
||||||
|
<h2>Intenção</h2>
|
||||||
|
<textarea
|
||||||
|
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
|
||||||
|
ref={register}
|
||||||
|
name="intention"
|
||||||
|
defaultValue={question?.intention ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col mt-4">
|
||||||
|
<h2>Revisor</h2>
|
||||||
|
<ReviewerSelect />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { gql, useQuery } from "@apollo/client";
|
||||||
|
|
||||||
|
import { useFormProvider } from '../../FormContext'
|
||||||
|
import { Query, QuestionStatus, User } from "../../../../../__generated__/graphql-schema";
|
||||||
|
|
||||||
|
export const ReviewerFragment = gql`
|
||||||
|
fragment ReviewerFields on Question {
|
||||||
|
reviewer {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const REVIEWERS_QUERY = gql`
|
||||||
|
query ReviwersQuery {
|
||||||
|
reviewers {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
reviewer?: User
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReviewerSelect: FC<Props> = () => {
|
||||||
|
const { question, hooks: { register } } = useFormProvider()
|
||||||
|
const { loading, data } = useQuery<Query>(REVIEWERS_QUERY);
|
||||||
|
|
||||||
|
if (loading) return null;
|
||||||
|
|
||||||
|
const reviewers = data?.reviewers.nodes
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
ref={register}
|
||||||
|
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||||
|
name="reviewerUserId"
|
||||||
|
defaultValue={question?.reviewer?.id}
|
||||||
|
>
|
||||||
|
{(question?.status === undefined || question?.status === QuestionStatus.Draft) && <option />}
|
||||||
|
{reviewers?.map((review, index) => (
|
||||||
|
<option key={index} value={review?.id}>
|
||||||
|
{review?.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { FC, useState } from "react";
|
||||||
|
import { useQuery, gql } from "@apollo/client";
|
||||||
|
|
||||||
|
import { Query } from "../../../../../__generated__/graphql-schema";
|
||||||
|
import { useFormProvider } from '../../FormContext'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
subjectId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubjectFragment = gql`
|
||||||
|
fragment SubjectFields on Question {
|
||||||
|
subject {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SUBJECTS_QUERY = gql`
|
||||||
|
query SubjectQuery {
|
||||||
|
subjects {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
axis {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
category {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const SubjectSelect: FC<Props> = () => {
|
||||||
|
const { question, hooks: { register } } = useFormProvider()
|
||||||
|
const [selectedId, setSelectedId] = useState(question?.subject?.id);
|
||||||
|
|
||||||
|
const { loading, data } = useQuery<Query>(SUBJECTS_QUERY);
|
||||||
|
|
||||||
|
if (loading) return null;
|
||||||
|
|
||||||
|
const subjects = data?.subjects.nodes
|
||||||
|
|
||||||
|
const selectedSubject = data?.subjects.nodes?.find((subject) => subject?.id === selectedId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div>
|
||||||
|
<h2>Assunto</h2>
|
||||||
|
<select
|
||||||
|
ref={register}
|
||||||
|
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||||
|
name="subjectId"
|
||||||
|
defaultValue={question?.subject?.id ?? ""}
|
||||||
|
onChange={(e) => setSelectedId(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="" />
|
||||||
|
{subjects?.map((subject) => (
|
||||||
|
<option
|
||||||
|
key={`${subject?.name}-${subject?.id}`}
|
||||||
|
value={subject?.id}
|
||||||
|
>
|
||||||
|
{subject?.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="mt-4">
|
||||||
|
Eixo de Formação
|
||||||
|
<input
|
||||||
|
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
|
||||||
|
disabled
|
||||||
|
value={selectedSubject?.axis.name}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="mt-4">
|
||||||
|
Categoria
|
||||||
|
<input
|
||||||
|
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
|
||||||
|
disabled
|
||||||
|
value={selectedSubject?.category.name}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./FeaturesFormStep";
|
||||||
4
app/javascript/pages/question/Form/steps/index.ts
Normal file
4
app/javascript/pages/question/Form/steps/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./EnunciationFormStep";
|
||||||
|
export * from "./AnswerFormStep";
|
||||||
|
export * from "./DistractoresFormStep";
|
||||||
|
export * from "./FeaturesFromStep";
|
||||||
35
app/javascript/pages/question/List/List.tsx
Normal file
35
app/javascript/pages/question/List/List.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { FC, useState } from "react";
|
||||||
|
import { FaFilter } from "react-icons/fa";
|
||||||
|
|
||||||
|
import { Navigator } from "../../../components";
|
||||||
|
import { QuestionsFilter } from "./QuestionFilter";
|
||||||
|
import { QuestionsPainel } from "./QuestionsPainel";
|
||||||
|
import { FiltersProvider } from './QuestionFilter/QuestionsFilterProvider'
|
||||||
|
|
||||||
|
export const List: FC = () => {
|
||||||
|
const [filterOpen, setFilterOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FiltersProvider>
|
||||||
|
<Navigator newQuestion={true}>
|
||||||
|
<li className={"hover:text-white ml-auto"}>
|
||||||
|
<button onClick={() => setFilterOpen(true)} className="flex">
|
||||||
|
<FaFilter className="my-auto" />
|
||||||
|
<span className="pl-3">Filtros</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</Navigator>
|
||||||
|
<QuestionsFilter
|
||||||
|
isOpen={filterOpen}
|
||||||
|
setIsOpen={setFilterOpen}
|
||||||
|
/>
|
||||||
|
<div className="bg-gray-100 w-full">
|
||||||
|
<main className="sm:px-8 rounded-t-xlg">
|
||||||
|
<div className="mx-2 sm:mx-0 sm:mr-4">
|
||||||
|
<QuestionsPainel />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</FiltersProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { Dispatch, FC, SetStateAction } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setChanged: Dispatch<SetStateAction<boolean>>
|
||||||
|
register: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthorshipFilter: FC<Props> = ({ setChanged, register }) => {
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: "Qualquer",
|
||||||
|
value: 'null'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Própria",
|
||||||
|
value: 'true'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Terceiro",
|
||||||
|
value: 'false',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-2 sm:mt-0 flex flex-col">
|
||||||
|
<h3 className="font-bold mb-1">Autoria</h3>
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-2 sm:flex sm:flex-col"
|
||||||
|
key={`filter-group-authorship`}
|
||||||
|
>
|
||||||
|
{options.map(({ value, label }, index) => (
|
||||||
|
<span className="mr-1 mb-2 sm:mb-0 sm:mr-0" key={label}>
|
||||||
|
<input
|
||||||
|
ref={register}
|
||||||
|
type="radio"
|
||||||
|
name="authorship"
|
||||||
|
value={value}
|
||||||
|
id={value}
|
||||||
|
defaultChecked={!index}
|
||||||
|
/>
|
||||||
|
<label htmlFor={value} className="ml-2">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { Dispatch, FC, SetStateAction } from 'react'
|
||||||
|
|
||||||
|
import { range } from '../../../../utils/math'
|
||||||
|
import { useFiltersProvider } from './QuestionsFilterProvider'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
register: any
|
||||||
|
setChanged: Dispatch<SetStateAction<boolean>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const CURRENT_YEAR = new Date().getFullYear()
|
||||||
|
|
||||||
|
const YEARS = range(1900, CURRENT_YEAR + 1).reverse()
|
||||||
|
|
||||||
|
export const QuestionsAuthorshipTypeFilter: FC<Props> = ({ register, setChanged }) => {
|
||||||
|
const { where } = useFiltersProvider()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
ref={register}
|
||||||
|
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||||
|
name="authorshipYear"
|
||||||
|
defaultValue={where.authorshipYear ?? ""}
|
||||||
|
onClick={() => setChanged(true)}
|
||||||
|
>
|
||||||
|
<option value="" />
|
||||||
|
{YEARS.map((year) => (
|
||||||
|
<option
|
||||||
|
key={`questionYear-${year}`}
|
||||||
|
value={year}
|
||||||
|
>
|
||||||
|
{year}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import React, { Dispatch, FC, SetStateAction, useRef, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import {
|
||||||
|
QuestionBloomTaxonomy,
|
||||||
|
QuestionCheckType,
|
||||||
|
QuestionDifficulty,
|
||||||
|
} from "../../../../__generated__/graphql-schema";
|
||||||
|
import { CHECK_TYPE, BLOOM_TAXONOMY, DIFFICULTY } from "../../../../utils/types";
|
||||||
|
|
||||||
|
import { Button, Modal } from "../../../../components";
|
||||||
|
import { useFiltersProvider } from "./QuestionsFilterProvider";
|
||||||
|
import { QuestionsSubjectFilter } from './QuestionsSubjectFilter'
|
||||||
|
import { QuestionsAuthorshipTypeFilter } from "./QuestionsAuthorshipTypeFilter";
|
||||||
|
import { AuthorshipFilter } from "./AuthorshipFilter";
|
||||||
|
|
||||||
|
type FilterGroupProps = {
|
||||||
|
title: string;
|
||||||
|
register: any;
|
||||||
|
options: {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
selecteds: any[];
|
||||||
|
setChanged: Dispatch<SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilterGroup: FC<FilterGroupProps> = ({
|
||||||
|
title,
|
||||||
|
options,
|
||||||
|
register,
|
||||||
|
selecteds,
|
||||||
|
setChanged,
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
<div className="mt-2 sm:mt-0 flex flex-col">
|
||||||
|
<h3 className="font-bold mb-1">{title}</h3>
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-2 sm:flex sm:flex-col"
|
||||||
|
key={`filter-group-${title}`}
|
||||||
|
>
|
||||||
|
{options.map(({ value, label }) => (
|
||||||
|
<span className="mr-1 mb-2 sm:mb-0 sm:mr-0" key={value}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name={value}
|
||||||
|
ref={register}
|
||||||
|
id={value}
|
||||||
|
defaultChecked={selecteds.includes(value)}
|
||||||
|
onClick={() => setChanged(true)}
|
||||||
|
/>
|
||||||
|
<label htmlFor={value} className="ml-2">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (state: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QuestionsFilter: FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||||
|
const { handleSubmit, register, reset } = useForm();
|
||||||
|
const { where, setWhere } = useFiltersProvider();
|
||||||
|
const { difficulty, checkType, bloomTaxonomy } = where;
|
||||||
|
const [changed, setChanged] = useState(false);
|
||||||
|
const submitRef = useRef<HTMLInputElement>()
|
||||||
|
|
||||||
|
const onSubmit = (inputs: any) => {
|
||||||
|
const valuesFromCheckType = CHECK_TYPE.filter(
|
||||||
|
({ value }) => inputs[value]
|
||||||
|
).map(({ value }) => value) as QuestionCheckType[];
|
||||||
|
|
||||||
|
const valuesFromBloomTaxonomy = BLOOM_TAXONOMY.filter(
|
||||||
|
({ value }) => inputs[value]
|
||||||
|
).map(({ value }) => value) as QuestionBloomTaxonomy[];
|
||||||
|
|
||||||
|
const valuesFromDifficulty = DIFFICULTY.filter(
|
||||||
|
({ value }) => inputs[value]
|
||||||
|
).map(({ value }) => value) as QuestionDifficulty[];
|
||||||
|
|
||||||
|
const removeKeysWithUndefiend = (obj: any) => {
|
||||||
|
for (var propName in obj) {
|
||||||
|
if (obj[propName] === null || obj[propName] === undefined) {
|
||||||
|
delete obj[propName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
setWhere({
|
||||||
|
unifesoAuthorship: inputs.authorship === 'null' ? null : inputs.authorship === 'true',
|
||||||
|
...removeKeysWithUndefiend({
|
||||||
|
checkType: valuesFromCheckType.length ? valuesFromCheckType : undefined,
|
||||||
|
bloomTaxonomy: valuesFromBloomTaxonomy.length
|
||||||
|
? valuesFromBloomTaxonomy
|
||||||
|
: undefined,
|
||||||
|
difficulty: valuesFromDifficulty.length
|
||||||
|
? valuesFromDifficulty
|
||||||
|
: undefined,
|
||||||
|
subjectId: inputs.subjectId === "" ? undefined : inputs.subjectId,
|
||||||
|
authorshipYear: inputs.authorshipYear === "" ? undefined : [inputs.authorshipYear],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
setChanged(false);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClean = () => {
|
||||||
|
setChanged(false);
|
||||||
|
setWhere({});
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Filtros"
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
isOpen={isOpen}
|
||||||
|
buttons={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleClean()}
|
||||||
|
disabled={!changed}
|
||||||
|
className={`${changed ? 'opacity-1' : 'opacity-0'}`}
|
||||||
|
>
|
||||||
|
Limpar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setIsOpen(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" onClick={() => submitRef.current?.click()}>
|
||||||
|
Aplicar
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:gap-8 lg:grid-cols-2">
|
||||||
|
<div className="mt-2 sm:mt-0 flex flex-col">
|
||||||
|
<h3 className="font-bold mb-1">Assunto</h3>
|
||||||
|
<div className="grid grid-cols-2 sm:flex sm:flex-col">
|
||||||
|
<QuestionsSubjectFilter register={register} setChanged={setChanged} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 sm:mt-0 flex flex-col">
|
||||||
|
<h3 className="font-bold mb-1">Ano</h3>
|
||||||
|
<div className="grid grid-cols-2 sm:flex sm:flex-col">
|
||||||
|
<QuestionsAuthorshipTypeFilter register={register} setChanged={setChanged} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FilterGroup
|
||||||
|
title="Tipo"
|
||||||
|
register={register}
|
||||||
|
options={CHECK_TYPE}
|
||||||
|
selecteds={(checkType ?? []) as QuestionCheckType[]}
|
||||||
|
setChanged={setChanged}
|
||||||
|
/>
|
||||||
|
<FilterGroup
|
||||||
|
title="Habilidade Cognitiva"
|
||||||
|
register={register}
|
||||||
|
options={BLOOM_TAXONOMY}
|
||||||
|
selecteds={(bloomTaxonomy ?? []) as QuestionBloomTaxonomy[]}
|
||||||
|
setChanged={setChanged}
|
||||||
|
/>
|
||||||
|
<FilterGroup
|
||||||
|
title="Grau de Dificuldade"
|
||||||
|
register={register}
|
||||||
|
options={DIFFICULTY}
|
||||||
|
selecteds={(difficulty ?? []) as QuestionDifficulty[]}
|
||||||
|
setChanged={setChanged}
|
||||||
|
/>
|
||||||
|
<AuthorshipFilter register={register} setChanged={setChanged} />
|
||||||
|
<input hidden type="submit" ref={submitRef as any} />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal >
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
FC,
|
||||||
|
useContext,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
|
import { QuestionWhereInput } from '../../../../__generated__/graphql-schema'
|
||||||
|
|
||||||
|
type ProviderValue = {
|
||||||
|
where: QuestionWhereInput
|
||||||
|
setWhere: Dispatch<SetStateAction<QuestionWhereInput>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const FiltersContext = createContext<ProviderValue | null>(null)
|
||||||
|
|
||||||
|
export const useFiltersProvider = () => {
|
||||||
|
const context = useContext(FiltersContext)
|
||||||
|
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error('You probably forgot to put <FiltersProvider>.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FiltersProvider: FC = ({ children }) => {
|
||||||
|
const [where, setWhere] = useState<QuestionWhereInput>({})
|
||||||
|
|
||||||
|
const providerValue = useMemo(() => ({ where, setWhere }), [
|
||||||
|
where,
|
||||||
|
setWhere,
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FiltersContext.Provider value={providerValue}>
|
||||||
|
{children}
|
||||||
|
</FiltersContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import React, { Dispatch, FC, SetStateAction } from 'react'
|
||||||
|
import { gql, useQuery } from '@apollo/client'
|
||||||
|
|
||||||
|
import { Query } from '../../../../__generated__/graphql-schema'
|
||||||
|
import { useFiltersProvider } from './QuestionsFilterProvider'
|
||||||
|
|
||||||
|
const SUBJECTS_QUERY = gql`
|
||||||
|
query {
|
||||||
|
subjects {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
register: any
|
||||||
|
setChanged: Dispatch<SetStateAction<boolean>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuestionsSubjectFilter: FC<Props> = ({ register, setChanged }) => {
|
||||||
|
const { where } = useFiltersProvider();
|
||||||
|
const { loading, data } = useQuery<Query>(SUBJECTS_QUERY)
|
||||||
|
|
||||||
|
if (loading) return null
|
||||||
|
|
||||||
|
const subjects = data?.subjects.nodes
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
ref={register}
|
||||||
|
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||||
|
name="subjectId"
|
||||||
|
defaultValue={where.subjectId ?? ""}
|
||||||
|
onClick={() => setChanged(true)}
|
||||||
|
>
|
||||||
|
<option value="" />
|
||||||
|
{subjects?.map((subject) => (
|
||||||
|
<option
|
||||||
|
key={`${subject?.name}-${subject?.id}`}
|
||||||
|
value={subject?.id}
|
||||||
|
>
|
||||||
|
{subject?.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./QuestionsFilter";
|
||||||
|
export * from "./QuestionsFilterProvider";
|
||||||
131
app/javascript/pages/question/List/QuestionsList.tsx
Normal file
131
app/javascript/pages/question/List/QuestionsList.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import React, { FC, useState } from 'react'
|
||||||
|
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
|
||||||
|
import { MdModeEdit } from 'react-icons/md';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { Question, QuestionStatus } from '../../../__generated__/graphql-schema'
|
||||||
|
import { useCurrentUser } from '../../../contexts';
|
||||||
|
import { NodeId } from '../../../utils/graphql';
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
const EditIcon = styled(MdModeEdit)`
|
||||||
|
margin: auto;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
questions: Question[]
|
||||||
|
title: string
|
||||||
|
pagination?: {
|
||||||
|
onNextPageClick: () => void
|
||||||
|
hasNextPage: boolean
|
||||||
|
hasPreviousPage: boolean
|
||||||
|
onPreviousPageClick: () => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuestionsListFragments = gql`
|
||||||
|
fragment QuestionFields on Question {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const QuestionsList: FC<Props> = ({ questions, title, pagination }) => {
|
||||||
|
const { user } = useCurrentUser()
|
||||||
|
const [pageCount, setPageCount] = useState(1)
|
||||||
|
|
||||||
|
const formatDate = (stringDate: string) => new Date(stringDate).toLocaleDateString()
|
||||||
|
|
||||||
|
const handleOnNextPageClick = () => {
|
||||||
|
if (pagination?.hasNextPage) {
|
||||||
|
pagination.onNextPageClick()
|
||||||
|
setPageCount(pageCount + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleOnPreviousPageClick = () => {
|
||||||
|
if (pagination?.hasPreviousPage) {
|
||||||
|
pagination.onPreviousPageClick()
|
||||||
|
setPageCount(pageCount - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-200 p-4 rounded my-2">
|
||||||
|
<div className="flex">
|
||||||
|
<h2 className="text-gray-500 font-medium text-xl">{title}</h2>
|
||||||
|
<div className="ml-auto text-sm sm:text-base text-gray-700">
|
||||||
|
<button
|
||||||
|
className="p-2"
|
||||||
|
onClick={handleOnPreviousPageClick}
|
||||||
|
style={{ visibility: (pagination?.hasPreviousPage ? 'visible' : 'hidden') }}
|
||||||
|
>
|
||||||
|
<FaArrowLeft />
|
||||||
|
</button>
|
||||||
|
Página: {pageCount}
|
||||||
|
<button
|
||||||
|
className="p-2"
|
||||||
|
onClick={handleOnNextPageClick}
|
||||||
|
style={{ visibility: (pagination?.hasNextPage ? 'visible' : 'hidden') }}
|
||||||
|
>
|
||||||
|
<FaArrowRight />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className="border-t border-gray-400 m-px" />
|
||||||
|
<div className="p-2 text-sm">
|
||||||
|
{questions.length
|
||||||
|
? <div className="flex-col w-full sm:grid gap-4 sm:col-gap-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||||
|
{questions.map((question) => (
|
||||||
|
<div
|
||||||
|
key={`question-${question.id}`}
|
||||||
|
className="mx-1 sm:mx-0 mb-4 sm:mb-0 border-l-8 border-primary-light flex bg-white hover:bg-unifeso-50 rounded shadow hover:shadow-md cursor-pointer group transition-all duration-500"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="flex flex-col w-full px-3 py-2"
|
||||||
|
to={`/questions/${question.id}/${(question.user.id === user?.id ? '' : 'review')}`}
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
{`# ${NodeId.decode(question.id).id}`}
|
||||||
|
</h2>
|
||||||
|
<div className="text-sm text-gray-700 flex flex-col flex-wrap justify-between">
|
||||||
|
<span>
|
||||||
|
Atualizada em:
|
||||||
|
{" "}
|
||||||
|
{formatDate(question.updatedAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
{(question.user.id === user?.id && question.status !== QuestionStatus.Registered) &&
|
||||||
|
<div
|
||||||
|
className="flex flex-col relative flex-grow justify-center"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="group-hover:block absolute bg-gray-300 hover:bg-primary-normal text-gray-500 hover:text-gray-100 hover:shadow-lg rounded-full p-2 cursor-pointer shadow-inner transition-all duration-500"
|
||||||
|
style={{ left: '-1.5rem' }}
|
||||||
|
to={`/questions/${question.id}/edit`}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
: <div className="grid text-gray-800" style={{ placeItems: 'center' }}>
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-sm sm:text-base">
|
||||||
|
Nenhuma questão.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
app/javascript/pages/question/List/QuestionsPainel.tsx
Normal file
22
app/javascript/pages/question/List/QuestionsPainel.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import { QuestionStatus } from '../../../__generated__/graphql-schema'
|
||||||
|
import { useFiltersProvider } from './QuestionFilter/QuestionsFilterProvider'
|
||||||
|
import { QuestionsQuery } from './QuestionsQuery'
|
||||||
|
import { QuestionsRevisedQuery } from './QuestionsRevisedQuery'
|
||||||
|
import { QuestionsWaitingReviewQuery } from './QuestionsWaitingReviewQuery'
|
||||||
|
|
||||||
|
export const QuestionsPainel: FC = () => {
|
||||||
|
const { where } = useFiltersProvider()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<QuestionsWaitingReviewQuery title="Aguardando seu Parecer" />
|
||||||
|
<QuestionsQuery title="Aguardando Parecer do Revisor" where={where} status={QuestionStatus.WaitingReview} />
|
||||||
|
<QuestionsQuery title="Pendentes de Alterações" where={where} status={QuestionStatus.WithRequestedChanges} />
|
||||||
|
<QuestionsQuery title="Rascunhos" where={where} status={QuestionStatus.Draft} />
|
||||||
|
<QuestionsQuery title="Aprovadas" where={where} status={QuestionStatus.Approved} />
|
||||||
|
<QuestionsQuery title="Registradas" where={where} status={QuestionStatus.Registered} />
|
||||||
|
<QuestionsRevisedQuery title="Revisadas por Você" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
95
app/javascript/pages/question/List/QuestionsQuery.tsx
Normal file
95
app/javascript/pages/question/List/QuestionsQuery.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import React, { FC, useState } from 'react'
|
||||||
|
|
||||||
|
import { PageInfo, Query, Question, QuestionWhereInput, QuestionStatus } from '../../../__generated__/graphql-schema';
|
||||||
|
import { gql, useQuery } from '@apollo/client';
|
||||||
|
import { QuestionsList, QuestionsListFragments } from './QuestionsList'
|
||||||
|
import { useCurrentUser } from '../../../contexts';
|
||||||
|
|
||||||
|
const QUESTIONS_QUERY = gql`
|
||||||
|
${QuestionsListFragments}
|
||||||
|
query QuestionsQuery($first: Int!, $after: String, $before: String, $where: QuestionWhereInput) {
|
||||||
|
questions (
|
||||||
|
first: $first,
|
||||||
|
after: $after,
|
||||||
|
before: $before,
|
||||||
|
where: $where
|
||||||
|
) {
|
||||||
|
nodes {
|
||||||
|
... QuestionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const PAGE_SIZE = 4
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
where?: QuestionWhereInput
|
||||||
|
status?: QuestionStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuestionsQuery: FC<Props> = ({ title, where, status }) => {
|
||||||
|
const { user } = useCurrentUser()
|
||||||
|
|
||||||
|
const [questions, setQuestions] = useState<Question[]>([])
|
||||||
|
const [pageInfo, setPageInfo] = useState<PageInfo | undefined>()
|
||||||
|
|
||||||
|
const updateQuestions = (queryResult: Query) => {
|
||||||
|
const { questions: questionConnection } = queryResult
|
||||||
|
|
||||||
|
setQuestions(questionConnection.nodes as Question[])
|
||||||
|
setPageInfo(questionConnection.pageInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereInput = {
|
||||||
|
status,
|
||||||
|
userId: user?.id,
|
||||||
|
...where
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fetchMore } = useQuery<Query>(QUESTIONS_QUERY, {
|
||||||
|
onCompleted: (response) => {
|
||||||
|
updateQuestions(response)
|
||||||
|
},
|
||||||
|
variables: {
|
||||||
|
first: PAGE_SIZE,
|
||||||
|
where: whereInput,
|
||||||
|
},
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
})
|
||||||
|
|
||||||
|
const onNextPageClick = () => {
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
first: PAGE_SIZE,
|
||||||
|
after: pageInfo?.endCursor,
|
||||||
|
},
|
||||||
|
}).then(({ data }) => {
|
||||||
|
updateQuestions(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPreviousPageClick = () => {
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
first: PAGE_SIZE,
|
||||||
|
before: pageInfo?.startCursor,
|
||||||
|
},
|
||||||
|
}).then(({ data }) => {
|
||||||
|
updateQuestions(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QuestionsList
|
||||||
|
title={title}
|
||||||
|
questions={questions}
|
||||||
|
pagination={{
|
||||||
|
onNextPageClick: onNextPageClick,
|
||||||
|
hasNextPage: pageInfo?.hasNextPage ?? false,
|
||||||
|
hasPreviousPage: pageInfo?.hasPreviousPage ?? false,
|
||||||
|
onPreviousPageClick: onPreviousPageClick
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
};
|
||||||
89
app/javascript/pages/question/List/QuestionsRevisedQuery.tsx
Normal file
89
app/javascript/pages/question/List/QuestionsRevisedQuery.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { FC, useState } from 'react'
|
||||||
|
|
||||||
|
import { PageInfo, Query, Question, ReviewRequest, User } from '../../../__generated__/graphql-schema';
|
||||||
|
import { gql, useQuery } from '@apollo/client';
|
||||||
|
import { QuestionsList, QuestionsListFragments } from './QuestionsList'
|
||||||
|
|
||||||
|
const QUESTIONS_QUERY = gql`
|
||||||
|
${QuestionsListFragments}
|
||||||
|
query QuestionsRevisedQuery($first: Int!, $after: String) {
|
||||||
|
currentUser {
|
||||||
|
id
|
||||||
|
inactiveReviewRequests(
|
||||||
|
first: $first,
|
||||||
|
after: $after
|
||||||
|
) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
question {
|
||||||
|
... QuestionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const PAGE_SIZE = 4
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuestionsRevisedQuery: FC<Props> = ({ title }) => {
|
||||||
|
const [questions, setQuestions] = useState<Question[]>([])
|
||||||
|
const [pageInfo, setPageInfo] = useState<PageInfo | undefined>()
|
||||||
|
|
||||||
|
const updateQuestions = (queryResult: Query) => {
|
||||||
|
const { currentUser } = queryResult
|
||||||
|
const { inactiveReviewRequests } = currentUser as User
|
||||||
|
const reviewRequests = inactiveReviewRequests.nodes as ReviewRequest[]
|
||||||
|
|
||||||
|
setQuestions(reviewRequests.map(item => item.question))
|
||||||
|
setPageInfo(inactiveReviewRequests.pageInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fetchMore } = useQuery<Query>(QUESTIONS_QUERY, {
|
||||||
|
onCompleted: (response) => {
|
||||||
|
updateQuestions(response)
|
||||||
|
},
|
||||||
|
variables: {
|
||||||
|
first: PAGE_SIZE,
|
||||||
|
},
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
})
|
||||||
|
|
||||||
|
const onNextPageClick = () => {
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
first: PAGE_SIZE,
|
||||||
|
after: pageInfo?.endCursor,
|
||||||
|
},
|
||||||
|
}).then(({ data }) => {
|
||||||
|
updateQuestions(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPreviousPageClick = () => {
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
first: PAGE_SIZE,
|
||||||
|
before: pageInfo?.startCursor,
|
||||||
|
},
|
||||||
|
}).then(({ data }) => {
|
||||||
|
updateQuestions(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QuestionsList
|
||||||
|
title={title}
|
||||||
|
questions={questions}
|
||||||
|
pagination={{
|
||||||
|
onNextPageClick: onNextPageClick,
|
||||||
|
hasNextPage: pageInfo?.hasNextPage ?? false,
|
||||||
|
hasPreviousPage: pageInfo?.hasPreviousPage ?? false,
|
||||||
|
onPreviousPageClick: onPreviousPageClick
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { FC, useState } from 'react'
|
||||||
|
|
||||||
|
import { PageInfo, Query, Question, ReviewRequest, User } from '../../../__generated__/graphql-schema';
|
||||||
|
import { gql, useQuery } from '@apollo/client';
|
||||||
|
import { QuestionsList, QuestionsListFragments } from './QuestionsList'
|
||||||
|
|
||||||
|
const QUESTIONS_QUERY = gql`
|
||||||
|
${QuestionsListFragments}
|
||||||
|
query QuestionsWaitingReviewQuery($first: Int!, $after: String) {
|
||||||
|
currentUser {
|
||||||
|
id
|
||||||
|
activeReviewRequests(
|
||||||
|
first: $first,
|
||||||
|
after: $after
|
||||||
|
) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
question {
|
||||||
|
... QuestionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const PAGE_SIZE = 4
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuestionsWaitingReviewQuery: FC<Props> = ({ title }) => {
|
||||||
|
const [questions, setQuestions] = useState<Question[]>([])
|
||||||
|
const [pageInfo, setPageInfo] = useState<PageInfo | undefined>()
|
||||||
|
|
||||||
|
const updateQuestions = (queryResult: Query) => {
|
||||||
|
const { currentUser } = queryResult
|
||||||
|
const { activeReviewRequests } = currentUser as User
|
||||||
|
const reviewRequests = activeReviewRequests.nodes as ReviewRequest[]
|
||||||
|
|
||||||
|
setQuestions(reviewRequests.map(item => item.question))
|
||||||
|
setPageInfo(activeReviewRequests.pageInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fetchMore } = useQuery<Query>(QUESTIONS_QUERY, {
|
||||||
|
onCompleted: (response) => {
|
||||||
|
updateQuestions(response)
|
||||||
|
},
|
||||||
|
variables: {
|
||||||
|
first: PAGE_SIZE,
|
||||||
|
},
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
})
|
||||||
|
|
||||||
|
const onNextPageClick = () => {
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
first: PAGE_SIZE,
|
||||||
|
after: pageInfo?.endCursor,
|
||||||
|
},
|
||||||
|
}).then(({ data }) => {
|
||||||
|
updateQuestions(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPreviousPageClick = () => {
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
first: PAGE_SIZE,
|
||||||
|
before: pageInfo?.startCursor,
|
||||||
|
},
|
||||||
|
}).then(({ data }) => {
|
||||||
|
updateQuestions(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QuestionsList
|
||||||
|
title={title}
|
||||||
|
questions={questions}
|
||||||
|
pagination={{
|
||||||
|
onNextPageClick: onNextPageClick,
|
||||||
|
hasNextPage: pageInfo?.hasNextPage ?? false,
|
||||||
|
hasPreviousPage: pageInfo?.hasPreviousPage ?? false,
|
||||||
|
onPreviousPageClick: onPreviousPageClick
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
};
|
||||||
1
app/javascript/pages/question/List/index.ts
Normal file
1
app/javascript/pages/question/List/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { List } from "./List";
|
||||||
87
app/javascript/pages/question/New/New.tsx
Normal file
87
app/javascript/pages/question/New/New.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import React, {useState} from "react";
|
||||||
|
import {useHistory} from "react-router";
|
||||||
|
import {gql, useMutation} from "@apollo/client";
|
||||||
|
|
||||||
|
import {AlertV2Props, Navigator} from "../../../components";
|
||||||
|
import {Form} from '../Form'
|
||||||
|
import {Mutation} from "../../../__generated__/graphql-schema";
|
||||||
|
import {QuestionRoutePaths} from "../../../routes";
|
||||||
|
|
||||||
|
const CREATE_QUESTION_MUTATION = gql`
|
||||||
|
mutation($input: CreateQuestionInput!) {
|
||||||
|
createQuestion(input: $input) {
|
||||||
|
question {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const New = () => {
|
||||||
|
const history = useHistory()
|
||||||
|
const [alert, setAlert] = useState<AlertV2Props>();
|
||||||
|
const [createQuestion] = useMutation<Mutation>(CREATE_QUESTION_MUTATION)
|
||||||
|
|
||||||
|
const onSubmit = (inputs: any) => {
|
||||||
|
createQuestion({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
question: inputs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
history.push(QuestionRoutePaths.index)
|
||||||
|
}).catch((error: string) => {
|
||||||
|
setAlert({
|
||||||
|
severity: "error",
|
||||||
|
text: `Erro ao criar questão. ${error}. Por favor, tente novamente.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
() => setAlert({severity: "error", text: ""}),
|
||||||
|
8000
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDraftSubmit = (inputs: any) => {
|
||||||
|
createQuestion({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
question: inputs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}).then(({data}) => {
|
||||||
|
setAlert({
|
||||||
|
severity: "success",
|
||||||
|
text: "Rascunho criado com sucesso",
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const id = data?.createQuestion?.question?.id
|
||||||
|
history.push(QuestionRoutePaths.edit.replace(':id', id ?? ''))
|
||||||
|
}, 3000);
|
||||||
|
}).catch((error: string) => {
|
||||||
|
setAlert({
|
||||||
|
severity: "error",
|
||||||
|
text: `Erro ao criar rascunho. ${error}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
() => setAlert(undefined),
|
||||||
|
8000
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navigator home={true}/>
|
||||||
|
<div className="bg-gray-100 w-full my-2">
|
||||||
|
<main>
|
||||||
|
<Form onSubmit={onSubmit} onDraftSubmit={onDraftSubmit} alert={alert}/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
1
app/javascript/pages/question/New/index.ts
Normal file
1
app/javascript/pages/question/New/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./New";
|
||||||
53
app/javascript/pages/question/Review/Review.tsx
Normal file
53
app/javascript/pages/question/Review/Review.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { gql, useQuery } from "@apollo/client";
|
||||||
|
|
||||||
|
import { ViewMode, ViewModeFragments, ReviewMessages, ReviewMessagesFragments } from "../shared";
|
||||||
|
import { Navigator } from "../../../components";
|
||||||
|
import { Query, Question } from "../../../__generated__/graphql-schema";
|
||||||
|
|
||||||
|
export const GET_QUESTION = gql`
|
||||||
|
${ViewModeFragments}
|
||||||
|
${ReviewMessagesFragments}
|
||||||
|
query Question($id: ID!) {
|
||||||
|
node(id: $id) {
|
||||||
|
__typename
|
||||||
|
... on Question {
|
||||||
|
id
|
||||||
|
...QuestionReadOnlyFields
|
||||||
|
...ReviewMessages_question
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Review: FC = () => {
|
||||||
|
const { id } = useParams<{ id: string }>()
|
||||||
|
const { loading, data, refetch } = useQuery<Query>(GET_QUESTION, {
|
||||||
|
variables: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
})
|
||||||
|
|
||||||
|
const question = data?.node as Question | null
|
||||||
|
|
||||||
|
if (loading || !question) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navigator home />
|
||||||
|
<div className="bg-gray-100 h-full w-full my-2">
|
||||||
|
<main className="flex px-5 max-w-screen-xl m-auto">
|
||||||
|
<div className="w-3/5">
|
||||||
|
<ViewMode questionData={question} />
|
||||||
|
</div>
|
||||||
|
<div className="w-2/5 ml-3">
|
||||||
|
<div className="my-3" />
|
||||||
|
<ReviewMessages question={question} refetch={refetch}/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
app/javascript/pages/question/Review/index.ts
Normal file
1
app/javascript/pages/question/Review/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Review } from "./Review";
|
||||||
188
app/javascript/pages/question/Show/Show.tsx
Normal file
188
app/javascript/pages/question/Show/Show.tsx
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import React, {FC, useState} from "react";
|
||||||
|
import {useParams, useHistory} from "react-router-dom";
|
||||||
|
import {MdDeleteForever, MdEdit, MdSave} from "react-icons/md";
|
||||||
|
import {useQuery, useMutation, gql} from "@apollo/client";
|
||||||
|
|
||||||
|
import {ViewMode, ViewModeFragments, ReviewMessages, ReviewMessagesFragments} from "../shared";
|
||||||
|
import {Navigator, Dialog} from "../../../components";
|
||||||
|
import {Mutation, Query, Question, QuestionStatus} from "../../../__generated__/graphql-schema";
|
||||||
|
import {AlertV2Props, AlertV2} from "../../../components/AlertV2";
|
||||||
|
import {NodeId} from "../../../utils/graphql";
|
||||||
|
import {QuestionRoutePaths} from "../../../routes";
|
||||||
|
|
||||||
|
export const GET_QUESTION = gql`
|
||||||
|
${ViewModeFragments}
|
||||||
|
${ReviewMessagesFragments}
|
||||||
|
query Question($id: ID!) {
|
||||||
|
node(id: $id) {
|
||||||
|
__typename
|
||||||
|
... on Question {
|
||||||
|
id
|
||||||
|
...QuestionReadOnlyFields
|
||||||
|
...ReviewMessages_question
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const FINISH_QUESTION = gql`
|
||||||
|
mutation FinishQuestion($id: ID!) {
|
||||||
|
finishQuestion (
|
||||||
|
input: {
|
||||||
|
questionId: $id
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
question {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
}
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DESTROY_QUESTION = gql`
|
||||||
|
mutation DestroyQuestion($id: ID!) {
|
||||||
|
destroyQuestion(
|
||||||
|
input: {
|
||||||
|
questionId: $id
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
deletedQuestionId
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Show: FC = () => {
|
||||||
|
const history = useHistory();
|
||||||
|
const {id} = useParams<{ id: string }>();
|
||||||
|
const [confirmRegister, setConfirmRegister] = useState(false)
|
||||||
|
const [confirmDestroy, setConfirmDestroy] = useState(false)
|
||||||
|
const [alert, setAlert] = useState<AlertV2Props>()
|
||||||
|
const [finishQuestion] = useMutation<Mutation>(FINISH_QUESTION)
|
||||||
|
const [destroyQuestion] = useMutation<Mutation>(DESTROY_QUESTION)
|
||||||
|
const {loading, data, refetch} = useQuery<Query>(GET_QUESTION, {
|
||||||
|
variables: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
});
|
||||||
|
const question = data?.node as Question | null
|
||||||
|
|
||||||
|
if (loading || !question) return null;
|
||||||
|
|
||||||
|
const recordId = NodeId.decode(question.id).id
|
||||||
|
|
||||||
|
const confirmEditQuestion = () => {
|
||||||
|
history.push(`/questions/${id}/edit`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditQuestion = () => {
|
||||||
|
confirmEditQuestion()
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegisterQuestion = async () => {
|
||||||
|
try {
|
||||||
|
await finishQuestion({variables: {id: recordId}})
|
||||||
|
|
||||||
|
setAlert({
|
||||||
|
text: 'Questão registrada com sucesso!',
|
||||||
|
severity: 'success',
|
||||||
|
})
|
||||||
|
} catch(error){
|
||||||
|
setAlert({
|
||||||
|
text: 'Algo inesperado aconteceu ao tentar registrar a questão.',
|
||||||
|
severity: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setConfirmRegister(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDestroyQuestion = async () => {
|
||||||
|
const {data: questionDestroyData } = await destroyQuestion({variables: {id: recordId}})
|
||||||
|
|
||||||
|
if (questionDestroyData?.destroyQuestion?.deletedQuestionId) {
|
||||||
|
history.push(QuestionRoutePaths.index)
|
||||||
|
} else {
|
||||||
|
setAlert({
|
||||||
|
text: 'Algo inesperado aconteceu ao tentar excluir a questão.',
|
||||||
|
severity: 'error',
|
||||||
|
})
|
||||||
|
setConfirmDestroy(false)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ACTIONS = {
|
||||||
|
edit: {
|
||||||
|
icon: <MdEdit className="my-auto"/>,
|
||||||
|
label: "Editar",
|
||||||
|
action: handleEditQuestion,
|
||||||
|
},
|
||||||
|
register: {
|
||||||
|
icon: <MdSave className="my-auto"/>,
|
||||||
|
label: "Registrar",
|
||||||
|
action: () => setConfirmRegister(true),
|
||||||
|
},
|
||||||
|
destroy: {
|
||||||
|
icon: <MdDeleteForever className="my-auto"/>,
|
||||||
|
label: 'Excluir',
|
||||||
|
action: () => setConfirmDestroy(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = (() => {
|
||||||
|
switch (question.status) {
|
||||||
|
case QuestionStatus.Registered:
|
||||||
|
return ([]);
|
||||||
|
case QuestionStatus.Approved:
|
||||||
|
return ([ACTIONS.edit, ACTIONS.register, ACTIONS.destroy])
|
||||||
|
default:
|
||||||
|
return ([ACTIONS.edit, ACTIONS.destroy])
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
isOpen={confirmDestroy}
|
||||||
|
setIsOpen={(value) => setConfirmDestroy(value)}
|
||||||
|
title="Confirmação de Exclusão"
|
||||||
|
text="Após a exclusão, a questão não poderá ser recuperada. Deseja continuar?"
|
||||||
|
onConfirmation={handleDestroyQuestion}
|
||||||
|
/>
|
||||||
|
<Dialog
|
||||||
|
isOpen={confirmRegister}
|
||||||
|
setIsOpen={(value) => setConfirmRegister(value)}
|
||||||
|
title="Confirmação de Registro"
|
||||||
|
text="Após o registro, a questão estará disponível para uso e não poderá mais ser editada ou excluída. Deseja continuar?"
|
||||||
|
onConfirmation={handleRegisterQuestion}
|
||||||
|
/>
|
||||||
|
<Navigator home>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<div key={`navigation-item-${index}`} className={`hover:text-white ${index === 0 ? "ml-auto" : ""}`}>
|
||||||
|
<button onClick={option.action} className="flex pl-4">
|
||||||
|
{option.icon}
|
||||||
|
<span className="pl-2">{option.label}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Navigator>
|
||||||
|
<div className="bg-gray-100 w-full my-2">
|
||||||
|
<main className="max-w-screen-xl m-auto">
|
||||||
|
<div className="flex">
|
||||||
|
{alert && <AlertV2 severity={alert.severity} text={alert.text}/>}
|
||||||
|
</div>
|
||||||
|
<div className="flex px-5">
|
||||||
|
<div className="w-3/5">
|
||||||
|
<ViewMode questionData={question}/>
|
||||||
|
</div>
|
||||||
|
<div className="w-2/5 ml-3">
|
||||||
|
<ReviewMessages question={question} refetch={refetch}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
app/javascript/pages/question/Show/index.ts
Normal file
1
app/javascript/pages/question/Show/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Show } from "./Show";
|
||||||
15
app/javascript/pages/question/formatInputs.ts
Normal file
15
app/javascript/pages/question/formatInputs.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Question } from "../../__generated__/graphql-schema";
|
||||||
|
|
||||||
|
export const formatInput = (inputs: any) =>
|
||||||
|
({
|
||||||
|
...inputs,
|
||||||
|
bloomTaxonomy:
|
||||||
|
inputs.bloomTaxonomy === "" ? undefined : inputs.bloomTaxonomy,
|
||||||
|
difficulty: inputs.difficulty === "" ? undefined : inputs.difficulty,
|
||||||
|
checkType: inputs.checkType === "" ? undefined : inputs.checkType,
|
||||||
|
subjectId: inputs.subjectId === "" ? undefined : inputs.subjectId,
|
||||||
|
reviewerUserId:
|
||||||
|
inputs.reviewerUserId === "" ? undefined : inputs.reviewerUserId,
|
||||||
|
alternatives: inputs.alternatives,
|
||||||
|
__nonused: undefined,
|
||||||
|
} as Question);
|
||||||
5
app/javascript/pages/question/index.ts
Normal file
5
app/javascript/pages/question/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "./New";
|
||||||
|
export * from "./Show";
|
||||||
|
export * from "./Review";
|
||||||
|
export * from "./Edit";
|
||||||
|
export * from "./List";
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { ApolloQueryResult, gql, OperationVariables } from "@apollo/client";
|
||||||
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
DocumentRemoveIcon
|
||||||
|
} from '@heroicons/react/outline';
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Card } from "../../../../components";
|
||||||
|
import { Query, Question, ReviewMessage, ReviewMessageFeedbackType } from "../../../../__generated__/graphql-schema";
|
||||||
|
import { ReviewMessageForm, ReviewMessageFormFragments } from "./ReviewMessagesForm";
|
||||||
|
|
||||||
|
|
||||||
|
const feedbackIcon = {
|
||||||
|
[ReviewMessageFeedbackType.Answer]: null,
|
||||||
|
[ReviewMessageFeedbackType.Approve]: <CheckCircleIcon className="w-5 text-green-800" />,
|
||||||
|
[ReviewMessageFeedbackType.RequestChanges]: <DocumentRemoveIcon className="w-5 text-red-800" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReviewMessageTitle: FC<{
|
||||||
|
feedback: ReviewMessage
|
||||||
|
}> = ({ feedback }) => (
|
||||||
|
<p className="flex">
|
||||||
|
{feedback.user.name}{' '} - {' '}
|
||||||
|
<span className="text-gray-700 pr-2">
|
||||||
|
{new Date(feedback.createdAt).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
{feedbackIcon[feedback.feedbackType]}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ReviewMessagesFragments = gql`
|
||||||
|
${ReviewMessageFormFragments}
|
||||||
|
fragment ReviewMessages_question on Question {
|
||||||
|
id
|
||||||
|
...ReviewMessageForm_question
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
reviewMessages {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
feedbackType
|
||||||
|
text
|
||||||
|
user {
|
||||||
|
name
|
||||||
|
avatarUrl
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ReviewMessages: FC<{
|
||||||
|
question: Question
|
||||||
|
refetch: (variables?: Partial<OperationVariables> | undefined) => Promise<ApolloQueryResult<Query>>
|
||||||
|
}> = ({ question, refetch }) => {
|
||||||
|
const reviewMessages = question.reviewMessages.nodes
|
||||||
|
const hasFeebacks = !!reviewMessages.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card className="mb-3" title="Histórico de Pareceres">
|
||||||
|
{hasFeebacks
|
||||||
|
? reviewMessages.map((item) => (
|
||||||
|
<div key={item.id}>
|
||||||
|
<ReviewMessageTitle feedback={item} />
|
||||||
|
<p className="p-2">
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: 'Essa questão não tem nenhum parecer ainda.'}
|
||||||
|
</Card>
|
||||||
|
<ReviewMessageForm question={question} refetch={refetch} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { ApolloQueryResult, gql, OperationVariables, useMutation } from "@apollo/client";
|
||||||
|
import React, { FC, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { Prompt, useHistory } from "react-router";
|
||||||
|
import { Button, Card } from "../../../../components";
|
||||||
|
import { useCurrentUser } from "../../../../contexts";
|
||||||
|
import { NodeId } from "../../../../utils/graphql";
|
||||||
|
import { Mutation, Query, Question, ReviewMessageFeedbackType } from "../../../../__generated__/graphql-schema";
|
||||||
|
|
||||||
|
export const REVIEW_FEEDBACK = [
|
||||||
|
{
|
||||||
|
label: "Aprovada",
|
||||||
|
description: "O revisor sugere que as observações enviadas no parecer sejam consideradas.",
|
||||||
|
value: ReviewMessageFeedbackType.Approve,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pendente de Alterações",
|
||||||
|
description: "O autor deve efetuar as alterações solicitadas no parecer e reenviar a questão ao revisor.",
|
||||||
|
value: ReviewMessageFeedbackType.RequestChanges,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ReviewMessageFormFragments = gql`
|
||||||
|
fragment ReviewMessageForm_question on Question {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const CREATE_REVIEW_MESSAGE_MUTATION = gql`
|
||||||
|
mutation($questionId: ID!, $feedbackType: ReviewMessageFeedbackType!, $text: String!) {
|
||||||
|
createReviewMessage(
|
||||||
|
input: {
|
||||||
|
message: {
|
||||||
|
questionId: $questionId
|
||||||
|
feedbackType: $feedbackType
|
||||||
|
text: $text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
reviewMessage {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ReviewMessageForm: FC<{
|
||||||
|
question: Question
|
||||||
|
refetch: (variables?: Partial<OperationVariables> | undefined) => Promise<ApolloQueryResult<Query>>
|
||||||
|
}> = ({ question, refetch }) => {
|
||||||
|
const [isChangesSaved, setIsChangesSaved] = useState(true)
|
||||||
|
const { register, handleSubmit } = useForm()
|
||||||
|
const history = useHistory();
|
||||||
|
const { user } = useCurrentUser()
|
||||||
|
|
||||||
|
const [createReviewMessage] = useMutation<Mutation['createReviewMessage']>(CREATE_REVIEW_MESSAGE_MUTATION)
|
||||||
|
|
||||||
|
const hasFeebacks = !!question.reviewMessages.nodes.length
|
||||||
|
const questionIsFromCurrentUser = user?.id === question.user.id
|
||||||
|
|
||||||
|
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.target.value !== '') {
|
||||||
|
setIsChangesSaved(false)
|
||||||
|
} else {
|
||||||
|
setIsChangesSaved(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitClick = () => {
|
||||||
|
setIsChangesSaved(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSubmit = async (inputs: {
|
||||||
|
feedbackType: ReviewMessageFeedbackType
|
||||||
|
text: string
|
||||||
|
}) => {
|
||||||
|
await createReviewMessage({
|
||||||
|
variables: {
|
||||||
|
text: inputs.text,
|
||||||
|
feedbackType: questionIsFromCurrentUser ? ReviewMessageFeedbackType.Answer : inputs.feedbackType,
|
||||||
|
questionId: NodeId.decode(question.id).id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await refetch()
|
||||||
|
|
||||||
|
history.push('/questions')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasFeebacks && questionIsFromCurrentUser) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Prompt
|
||||||
|
when={!isChangesSaved}
|
||||||
|
message='O parecer ainda não foi enviado, deseja continuar?'
|
||||||
|
/>
|
||||||
|
<Card title="Parecer" className="max-w-screen-md mx-auto">
|
||||||
|
<form onSubmit={handleSubmit(formSubmit)}>
|
||||||
|
<textarea
|
||||||
|
onChange={(e) => handleTextChange(e)}
|
||||||
|
className="w-full h-32 p-2 border-solid border-2 border-gray-700 rounded-md"
|
||||||
|
ref={register}
|
||||||
|
name="text"
|
||||||
|
/>
|
||||||
|
{!questionIsFromCurrentUser && REVIEW_FEEDBACK.map((item, index) => (
|
||||||
|
<div key={index} className="flex mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={item.value}
|
||||||
|
name="feedbackType"
|
||||||
|
ref={register({ required: true })}
|
||||||
|
value={item.value}
|
||||||
|
className="my-auto"
|
||||||
|
defaultChecked={index === 0}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={item.value}
|
||||||
|
className="flex flex-col pl-2 w-full"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
<p className="text-gray-700 text-sm">{item.description}</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="justify-end flex">
|
||||||
|
<Button type="primary" htmlType="submit" className="mt-4" onClick={handleSubmitClick}>
|
||||||
|
{questionIsFromCurrentUser ? 'Responder Parecer' : 'Enviar Parecer'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './ReviewMessages'
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user