Skip to content

Commit 0a21786

Browse files
author
Clay McGinnis
committed
Add Flier Upload to Events (#190)
* 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
1 parent d62a7f8 commit 0a21786

File tree

7 files changed

+167
-38
lines changed

7 files changed

+167
-38
lines changed

apps/admin-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"antd": "^3.12.4",
99
"apollo-boost": "^0.4.3",
1010
"apollo-link-context": "^1.0.18",
11+
"apollo-upload-client": "^11.0.0",
1112
"graphql": "^14.4.2",
1213
"react": "^16.7.0",
1314
"react-dom": "^16.7.0",

apps/admin-web/src/components/pages/tools/Events/EventForm.tsx

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1-
import React, { useGlobal } from "reactn";
1+
import React, { useGlobal, useState } from "reactn";
22

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

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

8-
import { Button, DatePicker, Form, Input, InputNumber } from "antd";
8+
import {
9+
Button,
10+
DatePicker,
11+
Form,
12+
Input,
13+
InputNumber,
14+
Upload,
15+
Icon
16+
} from "antd";
917

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

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

3242
const convertTimes: any = (data: any): any => {
3343
if (data.hasOwnProperty("dateRange") && data.dateRange) {
@@ -43,18 +53,23 @@ const EventFormBase: React.FC<any> = (props: any): JSX.Element => {
4353
props.form.validateFields((err: any, values: any) => {
4454
if (!err) {
4555
convertTimes(values);
56+
delete values.flier;
4657
if (editing) {
4758
const id: number = Number(values.id);
4859
delete values.id;
4960
updateEvent({
5061
refetchQueries: [{ query: GET_EVENTS }],
51-
variables: { data: values, id }
62+
variables: {
63+
flier: files.length > 0 ? files[0] : undefined,
64+
data: values,
65+
id
66+
}
5267
});
5368
// UPDATE THE NEW EVENT (values);
5469
} else {
5570
createEvent({
5671
refetchQueries: [{ query: GET_EVENTS }],
57-
variables: { data: values }
72+
variables: { data: values, flier: files[0] }
5873
});
5974
// CREATE THE NEW EVENT (values);
6075
}
@@ -63,6 +78,20 @@ const EventFormBase: React.FC<any> = (props: any): JSX.Element => {
6378
};
6479

6580
const { getFieldDecorator }: any = props.form;
81+
const params: UploadProps = {
82+
accept: ".jpg",
83+
multiple: false,
84+
fileList: files,
85+
onRemove: (): void => {
86+
setFiles([]);
87+
},
88+
beforeUpload: (newFile: UploadFile): boolean => {
89+
setFiles([newFile]);
90+
91+
// Uploading will be stopped with false or a rejected Promise returned.
92+
return false;
93+
}
94+
};
6695

6796
if (createLoading || updateLoading) {
6897
return <h1>Loading...</h1>;
@@ -138,17 +167,6 @@ const EventFormBase: React.FC<any> = (props: any): JSX.Element => {
138167
]
139168
})(<Input />)}
140169
</Form.Item>
141-
<Form.Item label="Flier Address">
142-
{getFieldDecorator("flierLink", {
143-
initialValue: newEvent.flierLink,
144-
rules: [
145-
{
146-
type: "url",
147-
message: "The input is not a valid URL."
148-
}
149-
]
150-
})(<Input />)}
151-
</Form.Item>
152170
<Form.Item
153171
label="Event Link"
154172
extra="Website, form, or other link to event."
@@ -163,6 +181,22 @@ const EventFormBase: React.FC<any> = (props: any): JSX.Element => {
163181
]
164182
})(<Input />)}
165183
</Form.Item>
184+
<Form.Item label="Event Flier">
185+
{getFieldDecorator("flier", {
186+
valuePropName: "file"
187+
})(
188+
<Upload.Dragger {...params}>
189+
<p className="ant-upload-drag-icon">
190+
<Icon type="inbox" />
191+
</p>
192+
<p className="ant-upload-text">
193+
Click or drag file to this area to upload
194+
</p>
195+
<p className="ant-upload-hint">Support for a single file upload.</p>
196+
</Upload.Dragger>
197+
)}
198+
</Form.Item>
199+
166200
<Form.Item label="Date and Time">
167201
{getFieldDecorator("dateRange", {
168202
initialValue: [

apps/admin-web/src/components/pages/tools/Events/helpers.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@ export const GET_EVENTS: any = gql`
2424
`;
2525

2626
export const CREATE_EVENT: any = gql`
27-
mutation CreateEvent($data: EventCreateInput!) {
28-
createEvent(data: $data) {
27+
mutation CreateEvent($flier: Upload, $data: EventCreateInput!) {
28+
createEvent(flier: $flier, data: $data) {
2929
eventTitle
3030
}
3131
}
3232
`;
3333

3434
export const UPDATE_EVENT: any = gql`
35-
mutation UpdateEvent($data: EventUpdateInput!, $id: Float!) {
36-
updateEvent(data: $data, id: $id) {
35+
mutation UpdateEvent($flier: Upload, $data: EventUpdateInput, $id: Float!) {
36+
updateEvent(flier: $flier, data: $data, id: $id) {
3737
eventTitle
3838
}
3939
}

apps/admin-web/src/utils/apollo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { InMemoryCache } from "apollo-cache-inmemory";
22
import { ApolloClient } from "apollo-client";
33
import { ApolloLink } from "apollo-link";
44
import { setContext } from "apollo-link-context";
5-
import { createHttpLink } from "apollo-link-http";
5+
import { createUploadLink } from "apollo-upload-client";
66

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

9-
const httpLink: ApolloLink = createHttpLink({
9+
const httpLink: ApolloLink = createUploadLink({
1010
uri: config.API_URI
1111
});
1212

apps/api/src/resources/Event/input.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Field, InputType, ObjectType } from "type-graphql";
22
import { Event } from "./entity";
3+
import { Readable } from "stream";
34

45
@InputType()
5-
export class EventCreateInput /*implements Partial<Event>*/ {
6+
export class EventCreateInput /*implements Partial<Event> */ {
67
@Field()
78
public eventTitle: string;
89

@@ -60,3 +61,17 @@ export class EventDeletePayload implements Partial<Event> {
6061
@Field({ nullable: true })
6162
public id: number;
6263
}
64+
65+
export class File {
66+
@Field(() => Readable)
67+
public createReadStream: () => Readable;
68+
69+
@Field()
70+
public filename: string;
71+
72+
@Field()
73+
public mimetype: string;
74+
75+
@Field()
76+
public encoding: string;
77+
}

apps/api/src/resources/Event/resolver.ts

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AuthenticationError } from "apollo-server";
1+
import { AuthenticationError, UserInputError } from "apollo-server";
22
import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from "type-graphql";
33
import {
44
DeepPartial,
@@ -7,10 +7,15 @@ import {
77
Repository
88
} from "typeorm";
99

10+
import { GraphQLUpload } from "graphql-upload";
11+
import * as fileType from "file-type";
12+
import { deleteFile, uploadFile } from "../../lib/files";
13+
1014
import { IContext } from "../../lib/interfaces";
1115
import { Sig } from "../Sig";
1216
import { User } from "../User";
1317
import { Event } from "./entity";
18+
import { File } from "./input";
1419
import {
1520
EventCreateInput,
1621
EventDeletePayload,
@@ -35,6 +40,10 @@ export class EventResolver {
3540
public async deleteEvent(
3641
@Arg("id", () => Number) id: number
3742
): Promise<INumber> {
43+
const event = await this.repository.findOneOrFail(id);
44+
if (event.flierLink) {
45+
deleteFile(event.flierLink);
46+
}
3847
await this.repository.delete(id);
3948

4049
return { id };
@@ -44,19 +53,55 @@ export class EventResolver {
4453
@Mutation(() => Event)
4554
public async updateEvent(
4655
@Arg("id", () => Number) id: number,
47-
@Arg("data", () => EventUpdateInput)
48-
input: DeepPartial<Event>
56+
@Arg("data", () => EventUpdateInput, { nullable: true })
57+
input: DeepPartial<Event>,
58+
@Arg("flier", () => GraphQLUpload, { nullable: true }) flier: File
4959
): Promise<Event> {
50-
if (input.hostSig) {
51-
const hostSig = await this.sigRepository.findOneOrFail({
52-
name: String(input.hostSig)
53-
});
54-
input.hostSig = hostSig;
60+
if (!input && !flier) {
61+
throw new UserInputError(
62+
"Please include either some new information or a flier to edit with."
63+
);
5564
}
5665

5766
const event = await this.repository.findOneOrFail(id);
58-
const updatedResource = this.repository.merge(event, { ...input });
67+
const updates: DeepPartial<Event> = input || {};
68+
69+
if (flier) {
70+
const passthrough = await fileType.stream(flier.createReadStream());
71+
if (
72+
!passthrough.fileType ||
73+
passthrough.fileType.ext !== "jpg" ||
74+
passthrough.fileType.mime !== "image/jpeg"
75+
) {
76+
throw new UserInputError("Error when parsing user input", {
77+
flier:
78+
"File uploaded was not detected as JPG. Contact [email protected] if you believe this is a mistake."
79+
});
80+
}
81+
82+
const origName: string =
83+
flier.filename.substr(0, flier.filename.lastIndexOf(".")) ||
84+
flier.filename;
85+
const encoded: string = encodeURIComponent(origName.replace(" ", "_"));
86+
const filename = `events/${encoded}_${event.id}.jpg`;
87+
const url = await uploadFile(
88+
flier.createReadStream(),
89+
filename,
90+
"image/jpeg"
91+
);
92+
if (event.flierLink) {
93+
deleteFile(event.flierLink);
94+
}
95+
updates.flierLink = url;
96+
}
97+
98+
if (input && input.hostSig) {
99+
updates.hostSig = await this.sigRepository.findOneOrFail({
100+
name: String(input.hostSig)
101+
});
102+
}
59103

104+
const updatedResource = this.repository.merge(event, { ...updates });
60105
return updatedResource.save();
61106
}
62107

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

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

76-
const hostSig: Sig = await this.sigRepository.findOneOrFail({
122+
input.hostSig = await this.sigRepository.findOneOrFail({
77123
name: String(input.hostSig)
78124
});
79-
input.hostSig = hostSig;
80-
const newResource = this.repository.create({ ...input, creator });
125+
const newResource = await this.repository
126+
.create({ ...input, creator })
127+
.save();
128+
129+
if (flier) {
130+
const passthrough = await fileType.stream(flier.createReadStream());
131+
if (
132+
!passthrough.fileType ||
133+
passthrough.fileType.ext !== "jpg" ||
134+
passthrough.fileType.mime !== "image/jpeg"
135+
) {
136+
throw new UserInputError("Error when parsing user input", {
137+
flier:
138+
"File uploaded was not detected as JPG. Contact [email protected] if you believe this is a mistake."
139+
});
140+
}
141+
142+
const origName: string =
143+
flier.filename.substr(0, flier.filename.lastIndexOf(".")) ||
144+
flier.filename;
145+
const encoded: string = encodeURIComponent(origName.replace(" ", "_"));
146+
const filename = `events/${encoded}_${newResource.id}.jpg`;
147+
const url = await uploadFile(
148+
flier.createReadStream(),
149+
filename,
150+
"image/jpeg"
151+
);
152+
newResource.flierLink = url;
153+
}
81154

82155
return newResource.save();
83156
}

apps/profile-web/src/generated/graphql.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export type Group = {
7676
name: Scalars['String'],
7777
users: Array<User>,
7878
permissions: Array<Permission>,
79+
redemptionCodes: Array<RedemptionCode>,
7980
};
8081

8182
export type MembershipProduct = {
@@ -101,7 +102,7 @@ export type Mutation = {
101102
updateEvent: Event,
102103
createEvent: Event,
103104
createPermission: Permission,
104-
createMembershipRedemptionCode: RedemptionCode,
105+
createRedemptionCode: RedemptionCode,
105106
redeemRedemptionCode: RedemptionCode,
106107
deleteResume: User,
107108
uploadResume: Resume,
@@ -165,8 +166,10 @@ export type MutationCreatePermissionArgs = {
165166
};
166167

167168

168-
export type MutationCreateMembershipRedemptionCodeArgs = {
169-
membershipType: MembershipTypes
169+
export type MutationCreateRedemptionCodeArgs = {
170+
groupIds?: Maybe<Array<Scalars['String']>>,
171+
permissionIds?: Maybe<Array<Scalars['String']>>,
172+
productTags?: Maybe<Array<Scalars['String']>>
170173
};
171174

172175

@@ -208,6 +211,7 @@ export type Permission = {
208211
__typename?: 'Permission',
209212
name: Scalars['ID'],
210213
users: Array<User>,
214+
redemptionCodes: Array<RedemptionCode>,
211215
};
212216

213217
export type PermissionCreateInput = {
@@ -283,6 +287,8 @@ export type RedemptionCode = {
283287
redeemed?: Maybe<Scalars['Boolean']>,
284288
expirationDate: Scalars['DateTime'],
285289
transaction: Transaction,
290+
permissions: Array<Permission>,
291+
groups: Array<Group>,
286292
};
287293

288294
export type Resume = {

0 commit comments

Comments
 (0)