Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add User Settings for Celebrating Completed Issues and Refactor User Formatting #27

Merged
merged 1 commit into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"scripts": {
"dev": "tsx watch src/index.ts",
"dev:migrate": "pnpm tsx src/umzug-runner.ts up",
"lint": "pnpm oxlint && pnpm eslint && tsc --noEmit",
"start": "node dist",
"format": "prettier --check --ignore-path ../.gitignore .",
Expand Down
3 changes: 3 additions & 0 deletions backend/src/__generated__/resolvers-types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions backend/src/db/migrations/20240112143058-add-settings-to-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

const TABLE_NAME = 'users';
const COLUMN_NAME = 'settings';

/** @type {import('sequelize-cli').Migration} */
export default {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn(TABLE_NAME, COLUMN_NAME, {
type: Sequelize.JSONB,
});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn(TABLE_NAME, COLUMN_NAME);
},
};
1 change: 1 addition & 0 deletions backend/src/db/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class User extends Model<InferAttributes<User>, InferCreationAttributes<U
declare lastName: CreationOptional<string | null>;
declare email: CreationOptional<string | null>;
declare avatarAssetId: CreationOptional<number | null>;
declare settings: CreationOptional<object | object[] | null>; // JSONB

declare static associate?: Associate;

Expand Down
4 changes: 4 additions & 0 deletions backend/src/db/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export default (sequelize: Sequelize) =>
key: 'id',
},
},
settings: {
type: DataTypes.JSONB,
field: 'settings',
},
createdAt: {
type: DataTypes.DATE,
field: 'created_at',
Expand Down
11 changes: 3 additions & 8 deletions backend/src/resolvers/issue/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '../../__generated__/resolvers-types.js';
import { Issue as IssueModel } from '../../db/models/types.js';
import { websocketBroadcast } from '../../services/ws-server.js';
import { formatUserForGraphql } from '../user/helpers';

const Query: QueryResolvers = {
issues: async (parent, { input: { projectId, id, search, searchOperator } }, { db, dataLoaderContext }) => {
Expand Down Expand Up @@ -404,10 +405,7 @@ const Issue: IssueResolvers = {

if (!reporterUser) return null;

return {
...reporterUser.toJSON(),
id: `${reporterUser.id}`,
};
return formatUserForGraphql(reporterUser);
},
assignee: async (parent, args, { db, dataLoaderContext, EXPECTED_OPTIONS_KEY }) => {
const databaseIssue = await db.Issue.findByPk(Number(parent.id), {
Expand All @@ -420,10 +418,7 @@ const Issue: IssueResolvers = {
[EXPECTED_OPTIONS_KEY]: dataLoaderContext,
});

return {
...user.toJSON(),
id: `${user.id}`,
};
return formatUserForGraphql(user);
},
project: async (parent, __, { db, dataLoaderContext, EXPECTED_OPTIONS_KEY }) => {
if (parent.project) return parent.project;
Expand Down
5 changes: 2 additions & 3 deletions backend/src/resolvers/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type Resolvers,
type ViewState,
} from '../../__generated__/resolvers-types.js';
import { formatUserForGraphql } from '../user/helpers';

const Query: QueryResolvers = {
projects: async (parent, args, { db, dataLoaderContext }) => {
Expand Down Expand Up @@ -74,9 +75,7 @@ const Query: QueryResolvers = {
id: `${databaseProject.id}`,
visibility: databaseProject.visibility as ProjectVisibility,
boards: undefined,
users: databaseProject.users
? databaseProject.users.map((user) => ({ ...user.toJSON(), id: `${user.id}` }))
: undefined,
users: databaseProject.users ? databaseProject.users.map(formatUserForGraphql) : undefined,
};
},
projectTags: async (parent, { input: { projectId, id, name } }, { db, dataLoaderContext }) => {
Expand Down
9 changes: 9 additions & 0 deletions backend/src/resolvers/user/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { User } from '../../db/models/types.js';

export const formatUserForGraphql = (user: User) => {
return {
...user.toJSON(),
id: `${user.id}`,
settings: user.settings ? JSON.stringify(user.settings) : null,
};
};
26 changes: 14 additions & 12 deletions backend/src/resolvers/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import { isUndefined } from 'lodash-es';

import type { MutationResolvers, QueryResolvers, UserResolvers } from '../../__generated__/resolvers-types.js';
import { Asset as AssetModel, User as UserModel } from '../../db/models/types.js';
import { Asset as AssetModel } from '../../db/models/types.js';
import { BUCKET_NAME } from '../../services/config.js';
import { minioClient } from '../../services/minio-client.js';

const formatUserResponse = (user: UserModel) => ({
...user.toJSON(),
id: `${user.id}`,
});
import { formatUserForGraphql } from './helpers.js';

const Query: QueryResolvers = {
users: async (parent, args, { db, dataLoaderContext }) => {
const dbUsers = await db.User.findAll();

dataLoaderContext.prime(dbUsers);

return dbUsers.map(formatUserResponse);
return dbUsers.map(formatUserForGraphql);
},
user: (parent, __, { db }) => {
// if (externalId) {
Expand All @@ -33,24 +29,30 @@ const Query: QueryResolvers = {
[EXPECTED_OPTIONS_KEY]: dataLoaderContext,
});

return formatUserResponse(dbUser);
return formatUserForGraphql(dbUser);
},
};

const Mutation: MutationResolvers = {
updateMe: async (parent, { input }, { db, user, dataLoaderContext }) => {
const { firstName, lastName } = input;
// We reload here in cases where the cached user is out of date from fastify auth cache
await user.reload();

const { firstName, lastName, settings } = input;
if (!user.id) throw new Error('User not found');

if (!isUndefined(firstName)) user.firstName = firstName;
if (!isUndefined(lastName)) user.lastName = lastName;
if (!isUndefined(settings)) user.settings = JSON.parse(settings);

await user.save();

dataLoaderContext.prime(user);

return formatUserResponse(user);
return formatUserForGraphql(user);
},
assignAssetAsAvatar: async (parent, { input }, { db, user, dataLoaderContext, EXPECTED_OPTIONS_KEY }) => {
// We reload here in cases where the cached user is out of date from fastify auth cache
await user.reload();

const { assetId } = input;
let findOldAvatarAsset: AssetModel;

Expand Down
5 changes: 3 additions & 2 deletions backend/src/server/fastify-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GraphQLError } from 'graphql';
import processRequest from 'graphql-upload/processRequest.mjs';

import db from '../db/index.js';
import { User as UserModel } from '../db/models/types.js';
import { cache } from '../services/cache.js';
import {
ALLOW_LOGIN_DOMAINS_LIST,
Expand All @@ -31,10 +32,10 @@ const addUserToRequest = async (request: CustomFastifyRequest, reply: FastifyRep

const cacheKey = hash(token);

const cachedUser = cache.get(cacheKey);
const cachedUser = <UserModel>cache.get(cacheKey);

if (cachedUser) {
request.user = db.User.build(cachedUser);
request.user = cachedUser;
return;
}

Expand Down
2 changes: 2 additions & 0 deletions backend/src/type-defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const typeDefs = gql`
name: String
email: String
avatarUrl: String
settings: String
}

type ProjectTag {
Expand Down Expand Up @@ -339,6 +340,7 @@ const typeDefs = gql`
input UpdateMeInput {
firstName: String
lastName: String
settings: String
}

input CreateIssueLinkInput {
Expand Down
42 changes: 11 additions & 31 deletions frontend/app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use client';

import React from 'react';
import { useMutation } from '@apollo/client';
import React from 'react';

import { AvatarUploadComponent } from '@/components/Avatar';
import { UserInfo } from '@/components/Profile/UserInfo';
import {
ASSIGN_ASSET_AS_AVATAR_MUTATION,
UPLOAD_ASSET_MUTATION,
} from '@/gql/gql-queries-mutations';
import { UserInfo } from '@/components/Profile/UserInfo';
import { AvatarUploadComponent } from '@/components/Avatar';
import { base64toBlob } from '@/services/utils';

export default function Profile() {
Expand Down Expand Up @@ -68,50 +69,29 @@ export default function Profile() {
<div className='md:flex md:items-center md:justify-between'>
<div className='space-y-12'>
<div className='border-b border-gray-900/10 pb-12'>
<div className='col-span-1 col-start-1 text-3xl font-extrabold text-black dark:text-white'>
<div className='col-span-1 col-start-1 text-3xl font-extrabold text-primary'>
Profile
</div>
<p className='mt-1 text-sm leading-6 text-gray-600'>
<p className='mt-1 text-sm leading-6 text-primary/60'>
This information will be displayed publicly so be careful what you
share.
</p>

<div className='mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6'>
<div className='sm:col-span-4'>
<label
htmlFor='username'
className='block text-sm font-medium leading-6 text-gray-900'
>
Username
</label>
<div className='mt-2'>
<div className='flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600 sm:max-w-md'>
<span className='flex select-none items-center pl-3 text-gray-500 sm:text-sm'>
user/
</span>
<input
type='text'
name='username'
id='username'
autoComplete='username'
className='block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6'
placeholder='janesmith'
/>
</div>
</div>
</div>

<div className='col-span-full'>
<h2 className='text-primary font-semibold leading-7 pb-4'>
Avatar
</h2>
<AvatarUploadComponent onAvatarSave={onAvatarSave} />
</div>
</div>
</div>

<div className='border-b border-gray-900/10 pb-12'>
<h2 className='text-base font-semibold leading-7 text-gray-900'>
<h2 className='text-primary font-semibold leading-7'>
Personal Information
</h2>
<p className='mt-1 text-sm leading-6 text-gray-600'>
<p className='mt-1 text-sm leading-6 text-primary/60'>
Use a permanent address where you can receive mail.
</p>

Expand Down
9 changes: 6 additions & 3 deletions frontend/components/Avatar/UploadComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, { useState } from 'react';
import dynamic from 'next/dynamic';
const Avatar = dynamic(() => import('react-avatar-edit'), { ssr: false });
import React, { useState } from 'react';

import { Button } from '@/components/Button';

const Avatar = dynamic(() => import('react-avatar-edit'), { ssr: false });

export const AvatarUploadComponent = ({
onAvatarSave,
}: {
Expand All @@ -24,14 +26,15 @@ export const AvatarUploadComponent = ({
}

return (
<div className='flex flex-row space-x-1'>
<div className='flex flex-row space-x-1 text-primary hover:cursor-pointer'>
<Avatar
width={600}
height={300}
onCrop={onCrop}
onClose={onClose}
onBeforeFileLoad={onBeforeFileLoad}
src={undefined}
labelStyle={{ color: undefined }}
/>
<br />
{preview && (
Expand Down
Loading