Skip to content

Commit

Permalink
Add Flier Upload to Events (#190)
Browse files Browse the repository at this point in the history
* Adding file to createEvent resolver

* Update event to accept a flier

* Fix updateEvent

* Connect the admin dashboard events form to the new flier upload stuff in api

* Fail if no info provided to updateEvent

* Fix filename in updateEvent
  • Loading branch information
Clay McGinnis committed Nov 6, 2019
1 parent d62a7f8 commit 0a21786
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 38 deletions.
1 change: 1 addition & 0 deletions apps/admin-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"antd": "^3.12.4",
"apollo-boost": "^0.4.3",
"apollo-link-context": "^1.0.18",
"apollo-upload-client": "^11.0.0",
"graphql": "^14.4.2",
"react": "^16.7.0",
"react-dom": "^16.7.0",
Expand Down
64 changes: 49 additions & 15 deletions apps/admin-web/src/components/pages/tools/Events/EventForm.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import React, { useGlobal } from "reactn";
import React, { useGlobal, useState } from "reactn";

import { useMutation } from "@apollo/react-hooks";
import moment from "moment";

import { CREATE_EVENT, GET_EVENTS, UPDATE_EVENT } from "./helpers";

import { Button, DatePicker, Form, Input, InputNumber } from "antd";
import {
Button,
DatePicker,
Form,
Input,
InputNumber,
Upload,
Icon
} from "antd";

import { IEvent } from "./interfaces";
import { UploadFile, UploadProps } from "antd/lib/upload/interface";

const { RangePicker }: any = DatePicker;
const { TextArea }: any = Input;
Expand All @@ -28,6 +37,7 @@ const EventFormBase: React.FC<any> = (props: any): JSX.Element => {
updateEvent,
{ loading: updateLoading, error: updateError, data: updateData }
]: any = useMutation(UPDATE_EVENT);
const [files, setFiles] = useState<UploadFile[]>([]);

const convertTimes: any = (data: any): any => {
if (data.hasOwnProperty("dateRange") && data.dateRange) {
Expand All @@ -43,18 +53,23 @@ const EventFormBase: React.FC<any> = (props: any): JSX.Element => {
props.form.validateFields((err: any, values: any) => {
if (!err) {
convertTimes(values);
delete values.flier;
if (editing) {
const id: number = Number(values.id);
delete values.id;
updateEvent({
refetchQueries: [{ query: GET_EVENTS }],
variables: { data: values, id }
variables: {
flier: files.length > 0 ? files[0] : undefined,
data: values,
id
}
});
// UPDATE THE NEW EVENT (values);
} else {
createEvent({
refetchQueries: [{ query: GET_EVENTS }],
variables: { data: values }
variables: { data: values, flier: files[0] }
});
// CREATE THE NEW EVENT (values);
}
Expand All @@ -63,6 +78,20 @@ const EventFormBase: React.FC<any> = (props: any): JSX.Element => {
};

const { getFieldDecorator }: any = props.form;
const params: UploadProps = {
accept: ".jpg",
multiple: false,
fileList: files,
onRemove: (): void => {
setFiles([]);
},
beforeUpload: (newFile: UploadFile): boolean => {
setFiles([newFile]);

// Uploading will be stopped with false or a rejected Promise returned.
return false;
}
};

if (createLoading || updateLoading) {
return <h1>Loading...</h1>;
Expand Down Expand Up @@ -138,17 +167,6 @@ const EventFormBase: React.FC<any> = (props: any): JSX.Element => {
]
})(<Input />)}
</Form.Item>
<Form.Item label="Flier Address">
{getFieldDecorator("flierLink", {
initialValue: newEvent.flierLink,
rules: [
{
type: "url",
message: "The input is not a valid URL."
}
]
})(<Input />)}
</Form.Item>
<Form.Item
label="Event Link"
extra="Website, form, or other link to event."
Expand All @@ -163,6 +181,22 @@ const EventFormBase: React.FC<any> = (props: any): JSX.Element => {
]
})(<Input />)}
</Form.Item>
<Form.Item label="Event Flier">
{getFieldDecorator("flier", {
valuePropName: "file"
})(
<Upload.Dragger {...params}>
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">
Click or drag file to this area to upload
</p>
<p className="ant-upload-hint">Support for a single file upload.</p>
</Upload.Dragger>
)}
</Form.Item>

<Form.Item label="Date and Time">
{getFieldDecorator("dateRange", {
initialValue: [
Expand Down
8 changes: 4 additions & 4 deletions apps/admin-web/src/components/pages/tools/Events/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ export const GET_EVENTS: any = gql`
`;

export const CREATE_EVENT: any = gql`
mutation CreateEvent($data: EventCreateInput!) {
createEvent(data: $data) {
mutation CreateEvent($flier: Upload, $data: EventCreateInput!) {
createEvent(flier: $flier, data: $data) {
eventTitle
}
}
`;

export const UPDATE_EVENT: any = gql`
mutation UpdateEvent($data: EventUpdateInput!, $id: Float!) {
updateEvent(data: $data, id: $id) {
mutation UpdateEvent($flier: Upload, $data: EventUpdateInput, $id: Float!) {
updateEvent(flier: $flier, data: $data, id: $id) {
eventTitle
}
}
Expand Down
4 changes: 2 additions & 2 deletions apps/admin-web/src/utils/apollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client";
import { ApolloLink } from "apollo-link";
import { setContext } from "apollo-link-context";
import { createHttpLink } from "apollo-link-http";
import { createUploadLink } from "apollo-upload-client";

import { config } from "../config";

const httpLink: ApolloLink = createHttpLink({
const httpLink: ApolloLink = createUploadLink({
uri: config.API_URI
});

Expand Down
17 changes: 16 additions & 1 deletion apps/api/src/resources/Event/input.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Field, InputType, ObjectType } from "type-graphql";
import { Event } from "./entity";
import { Readable } from "stream";

@InputType()
export class EventCreateInput /*implements Partial<Event>*/ {
export class EventCreateInput /*implements Partial<Event> */ {
@Field()
public eventTitle: string;

Expand Down Expand Up @@ -60,3 +61,17 @@ export class EventDeletePayload implements Partial<Event> {
@Field({ nullable: true })
public id: number;
}

export class File {
@Field(() => Readable)
public createReadStream: () => Readable;

@Field()
public filename: string;

@Field()
public mimetype: string;

@Field()
public encoding: string;
}
99 changes: 86 additions & 13 deletions apps/api/src/resources/Event/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AuthenticationError } from "apollo-server";
import { AuthenticationError, UserInputError } from "apollo-server";
import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from "type-graphql";
import {
DeepPartial,
Expand All @@ -7,10 +7,15 @@ import {
Repository
} from "typeorm";

import { GraphQLUpload } from "graphql-upload";
import * as fileType from "file-type";
import { deleteFile, uploadFile } from "../../lib/files";

import { IContext } from "../../lib/interfaces";
import { Sig } from "../Sig";
import { User } from "../User";
import { Event } from "./entity";
import { File } from "./input";
import {
EventCreateInput,
EventDeletePayload,
Expand All @@ -35,6 +40,10 @@ export class EventResolver {
public async deleteEvent(
@Arg("id", () => Number) id: number
): Promise<INumber> {
const event = await this.repository.findOneOrFail(id);
if (event.flierLink) {
deleteFile(event.flierLink);
}
await this.repository.delete(id);

return { id };
Expand All @@ -44,19 +53,55 @@ export class EventResolver {
@Mutation(() => Event)
public async updateEvent(
@Arg("id", () => Number) id: number,
@Arg("data", () => EventUpdateInput)
input: DeepPartial<Event>
@Arg("data", () => EventUpdateInput, { nullable: true })
input: DeepPartial<Event>,
@Arg("flier", () => GraphQLUpload, { nullable: true }) flier: File
): Promise<Event> {
if (input.hostSig) {
const hostSig = await this.sigRepository.findOneOrFail({
name: String(input.hostSig)
});
input.hostSig = hostSig;
if (!input && !flier) {
throw new UserInputError(
"Please include either some new information or a flier to edit with."
);
}

const event = await this.repository.findOneOrFail(id);
const updatedResource = this.repository.merge(event, { ...input });
const updates: DeepPartial<Event> = input || {};

if (flier) {
const passthrough = await fileType.stream(flier.createReadStream());
if (
!passthrough.fileType ||
passthrough.fileType.ext !== "jpg" ||
passthrough.fileType.mime !== "image/jpeg"
) {
throw new UserInputError("Error when parsing user input", {
flier:
"File uploaded was not detected as JPG. Contact acm@mst.edu if you believe this is a mistake."
});
}

const origName: string =
flier.filename.substr(0, flier.filename.lastIndexOf(".")) ||
flier.filename;
const encoded: string = encodeURIComponent(origName.replace(" ", "_"));
const filename = `events/${encoded}_${event.id}.jpg`;
const url = await uploadFile(
flier.createReadStream(),
filename,
"image/jpeg"
);
if (event.flierLink) {
deleteFile(event.flierLink);
}
updates.flierLink = url;
}

if (input && input.hostSig) {
updates.hostSig = await this.sigRepository.findOneOrFail({
name: String(input.hostSig)
});
}

const updatedResource = this.repository.merge(event, { ...updates });
return updatedResource.save();
}

Expand All @@ -65,19 +110,47 @@ export class EventResolver {
public async createEvent(
@Ctx() context: IContext,
@Arg("data", () => EventCreateInput)
input: DeepPartial<Event>
input: DeepPartial<Event>,
@Arg("flier", () => GraphQLUpload, { nullable: true }) flier?: File
): Promise<Event> {
const creator: User | undefined = context.state.user;

if (!creator) {
throw new AuthenticationError("Please login to access this resource.");
}

const hostSig: Sig = await this.sigRepository.findOneOrFail({
input.hostSig = await this.sigRepository.findOneOrFail({
name: String(input.hostSig)
});
input.hostSig = hostSig;
const newResource = this.repository.create({ ...input, creator });
const newResource = await this.repository
.create({ ...input, creator })
.save();

if (flier) {
const passthrough = await fileType.stream(flier.createReadStream());
if (
!passthrough.fileType ||
passthrough.fileType.ext !== "jpg" ||
passthrough.fileType.mime !== "image/jpeg"
) {
throw new UserInputError("Error when parsing user input", {
flier:
"File uploaded was not detected as JPG. Contact acm@mst.edu if you believe this is a mistake."
});
}

const origName: string =
flier.filename.substr(0, flier.filename.lastIndexOf(".")) ||
flier.filename;
const encoded: string = encodeURIComponent(origName.replace(" ", "_"));
const filename = `events/${encoded}_${newResource.id}.jpg`;
const url = await uploadFile(
flier.createReadStream(),
filename,
"image/jpeg"
);
newResource.flierLink = url;
}

return newResource.save();
}
Expand Down
12 changes: 9 additions & 3 deletions apps/profile-web/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export type Group = {
name: Scalars['String'],
users: Array<User>,
permissions: Array<Permission>,
redemptionCodes: Array<RedemptionCode>,
};

export type MembershipProduct = {
Expand All @@ -101,7 +102,7 @@ export type Mutation = {
updateEvent: Event,
createEvent: Event,
createPermission: Permission,
createMembershipRedemptionCode: RedemptionCode,
createRedemptionCode: RedemptionCode,
redeemRedemptionCode: RedemptionCode,
deleteResume: User,
uploadResume: Resume,
Expand Down Expand Up @@ -165,8 +166,10 @@ export type MutationCreatePermissionArgs = {
};


export type MutationCreateMembershipRedemptionCodeArgs = {
membershipType: MembershipTypes
export type MutationCreateRedemptionCodeArgs = {
groupIds?: Maybe<Array<Scalars['String']>>,
permissionIds?: Maybe<Array<Scalars['String']>>,
productTags?: Maybe<Array<Scalars['String']>>
};


Expand Down Expand Up @@ -208,6 +211,7 @@ export type Permission = {
__typename?: 'Permission',
name: Scalars['ID'],
users: Array<User>,
redemptionCodes: Array<RedemptionCode>,
};

export type PermissionCreateInput = {
Expand Down Expand Up @@ -283,6 +287,8 @@ export type RedemptionCode = {
redeemed?: Maybe<Scalars['Boolean']>,
expirationDate: Scalars['DateTime'],
transaction: Transaction,
permissions: Array<Permission>,
groups: Array<Group>,
};

export type Resume = {
Expand Down

0 comments on commit 0a21786

Please sign in to comment.