Skip to content

Commit

Permalink
feature: 受信したフィードバック一覧を表示するようにする
Browse files Browse the repository at this point in the history
  • Loading branch information
Karibash committed Dec 2, 2024
1 parent fded0cd commit 54985af
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 4 deletions.
14 changes: 14 additions & 0 deletions apps/web/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,27 @@ type User {
"""ユーザー名"""
name: String!

"""ユーザーが受信したフィードバック"""
receivedFeedbacks(after: String, before: String, first: Int, last: Int): UserReceivedFeedbacksConnection!

"""ユーザーに紐づくSlackユーザー"""
slackUsers: [SlackUser!]!

"""ユーザー種別"""
type: UserType!
}

type UserReceivedFeedbacksConnection {
edges: [UserReceivedFeedbacksConnectionEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}

type UserReceivedFeedbacksConnectionEdge {
cursor: String!
node: Feedback!
}

"""ユーザー種別"""
enum UserType {
designer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as styles from './page.css';
import { FeedbackCard } from '../../../../../components/domains/feedbacks/feedback-card';

import type { FC } from 'react';

export type UserDetailsFeedbacksLoadingPageProps = {
//
};

const UserDetailsFeedbacksLoadingPage: FC<UserDetailsFeedbacksLoadingPageProps> = () => {
return (
<div className={styles.wrapper}>
{Array.from({ length: 5 }).map((_, index) => (
<FeedbackCard key={index} fragment={undefined} />
))}
</div>
);
};

export default UserDetailsFeedbacksLoadingPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { style } from '@vanilla-extract/css';

export const wrapper = style({
display: 'flex',
flexDirection: 'column',
gap: '16px',
});
67 changes: 63 additions & 4 deletions apps/web/src/app/(authorized)/users/[userId]/@feedbacks/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
import * as styles from './page.css';
import { graphql } from '../../../../../../.graphql';
import { FeedbackCard } from '../../../../../components/domains/feedbacks/feedback-card';
import { InfiniteScroll } from '../../../../../components/elements/infinite-scroll';
import { graphqlExecutor } from '../../../../../graphql';

import type { InfiniteScrollFetcher } from '../../../../../components/elements/infinite-scroll';
import type { FC } from 'react';

export type UserDetailsReceivedFeedbacksPageProps = {
const limit = 10;

const UserDetailsFeedbacksPageReceivedQuery = graphql(/* GraphQL */ `
query UserDetailsFeedbacksPageReceived($userId: ID!, $limit: Int!, $cursor: String) {
userById(userId: $userId) {
receivedFeedbacks(first: $limit, after: $cursor) {
edges {
cursor
node {
id
...FeedbackCard
}
}
}
}
}
`);

export type UserDetailsFeedbacksPageProps = {
params: {
userId: string;
};
Expand All @@ -9,8 +34,42 @@ export type UserDetailsReceivedFeedbacksPageProps = {
};
};

const UserDetailsReceivedFeedbacksPage: FC<UserDetailsReceivedFeedbacksPageProps> = () => {
return null;
const UserDetailsFeedbacksPage: FC<UserDetailsFeedbacksPageProps> = async ({
params,
searchParams,
}) => {
if (searchParams.tab === 'sent') {
return null;
}

const data = await graphqlExecutor({
document: UserDetailsFeedbacksPageReceivedQuery,
variables: { userId: params.userId, limit },
});

const fetcher: InfiniteScrollFetcher = async ({ cursor }) => {
'use server';
const data = await graphqlExecutor({
document: UserDetailsFeedbacksPageReceivedQuery,
variables: { userId: params.userId, limit, cursor },
});

return data.userById.receivedFeedbacks.edges.map((edge) => ({
cursor: edge.cursor,
node: <FeedbackCard key={edge.node.id} fragment={edge.node} />,
}));
};

return (
<InfiniteScroll
className={styles.wrapper}
edges={data.userById.receivedFeedbacks.edges.map((edge) => ({
cursor: edge.cursor,
node: <FeedbackCard key={edge.node.id} fragment={edge.node} />,
}))}
fetcher={fetcher}
/>
);
};

export default UserDetailsReceivedFeedbacksPage;
export default UserDetailsFeedbacksPage;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { style } from '@vanilla-extract/css';
import { theme } from '../../../../themes';

export const content = style({
display: 'flex',
flexDirection: 'column',
gap: '16px',
marginTop: '16px',
});

Expand Down
62 changes: 62 additions & 0 deletions apps/web/src/graphql/resolvers/users/fields/received-feedbacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { resolveCursorConnection } from '@pothos/plugin-relay';
import * as v from 'valibot';

import { builder } from '../../../core/builder';
import { deserialize } from '../../../helpers/deserialize';
import { serialize } from '../../../helpers/serialize';
import { userReceivedFeedbacksCount } from '../../../usecases/users/fields/received-feedback-count';
import { userReceivedFeedbacks } from '../../../usecases/users/fields/received-feedbacks';
import { Feedback } from '../../feedbacks/types/feedback';
import { User } from '../types/user';

import type { ResolveCursorConnectionArgs } from '@pothos/plugin-relay';

builder.objectField(User, 'receivedFeedbacks', (t) => t.connection({
type: Feedback,
description: 'ユーザーが受信したフィードバック',
resolve: async (user, args) => {
const connection = await resolveCursorConnection(
{
args,
toCursor: (feedback) => serialize(feedback.cursor),
},
async ({ after, limit }: ResolveCursorConnectionArgs) => {
const result = await userReceivedFeedbacks({
userId: user.id,
limit: limit,
cursor: after ? deserialize(after, v.object({
id: v.string(),
createdAt: v.pipe(
v.string(),
v.isoTimestamp(),
v.transform((value) => new Date(value)),
),
})) : undefined,
});

if (result.isOk()) {
return result.value;
}

throw result.error;
},
);

const totalCount = async () => {
const result = await userReceivedFeedbacksCount({
userId: user.id,
});

if (result.isOk()) {
return result.value;
}

throw result.error;
};

return {
...connection,
totalCount,
};
},
}));
1 change: 1 addition & 0 deletions apps/web/src/graphql/resolvers/users/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './fields/received-feedbacks';
import './fields/slack-users';
import './queries/me';
import './queries/user-by-id';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { CustomError } from '@feedbackun/package-custom-error';
import { database, schema } from '@feedbackun/package-database';
import DataLoader from 'dataloader';
import { count, eq } from 'drizzle-orm';
import { ResultAsync } from 'neverthrow';

import { serialize } from '../../../helpers/serialize';
import { dataLoader } from '../../../plugins/dataloader';

const symbol = Symbol('UserReceivedFeedbacksCount');

export type UserReceivedFeedbacksCountInput = {
userId: string;
};

export class UserReceivedFeedbacksCountUnexpectedError extends CustomError({
name: 'UserReceivedFeedbacksCountUnexpectedError',
message: 'Failed to count received feedbacks for user.',
}) {}

export type UserReceivedFeedbacksCountError = (
| UserReceivedFeedbacksCountUnexpectedError
);

export type UserReceivedFeedbacksCount = (
input: UserReceivedFeedbacksCountInput,
) => ResultAsync<number, UserReceivedFeedbacksCountError>;

export const userReceivedFeedbacksCount: UserReceivedFeedbacksCount = (input) => {
const loader = dataLoader(symbol, () => new DataLoader<UserReceivedFeedbacksCountInput, number, string>(async (inputs) => {
const execute = async (input: UserReceivedFeedbacksCountInput): Promise<number> => {
const [row] = await database()
.select({ count: count() })
.from(schema.feedbacks)
.innerJoin(schema.slackUsers, eq(schema.slackUsers.id, schema.feedbacks.receiveSlackUserId))
.innerJoin(schema.users, eq(schema.users.id, schema.slackUsers.userId))
.where(eq(schema.users.id, input.userId));

return row?.count ?? 0;
};

return await Promise.all(inputs.map((input) => execute(input)));
}, { cacheKeyFn: serialize }));

return ResultAsync.fromThrowable(
() => loader.load(input),
(error) => new UserReceivedFeedbacksCountUnexpectedError({ cause: error }),
)();
};
85 changes: 85 additions & 0 deletions apps/web/src/graphql/usecases/users/fields/received-feedbacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { CustomError } from '@feedbackun/package-custom-error';
import { database, schema } from '@feedbackun/package-database';
import DataLoader from 'dataloader';
import { and, desc, eq, lt, or } from 'drizzle-orm';
import { ResultAsync } from 'neverthrow';

import { serialize } from '../../../helpers/serialize';
import { dataLoader } from '../../../plugins/dataloader';

import type { Feedback } from '../../feedbacks/types/feedback';

const symbol = Symbol('UserReceivedFeedbacks');

export type UserReceivedFeedbacksCursor = {
id: string;
createdAt: Date;
};

export type UserReceivedFeedbacksInput = {
userId: string;
limit: number;
cursor: UserReceivedFeedbacksCursor | undefined;
};

export class UserReceivedFeedbacksUnexpectedError extends CustomError({
name: 'UserReceivedFeedbacksUnexpectedError',
message: 'Failed to find received feedbacks for user.',
}) {}

export type UserReceivedFeedbacksError = (
| UserReceivedFeedbacksUnexpectedError
);

export type UserReceivedFeedbacksNode = Feedback & { cursor: UserReceivedFeedbacksCursor };

export type UserReceivedFeedbacks = (
input: UserReceivedFeedbacksInput,
) => ResultAsync<UserReceivedFeedbacksNode[], UserReceivedFeedbacksError>;

export const userReceivedFeedbacks: UserReceivedFeedbacks = (input) => {
const loader = dataLoader(symbol, () => new DataLoader<UserReceivedFeedbacksInput, UserReceivedFeedbacksNode[], string>(async (inputs) => {
const execute = async (input: UserReceivedFeedbacksInput): Promise<UserReceivedFeedbacksNode[]> => {
const filters: Parameters<typeof and> = [
eq(schema.users.id, input.userId),
];

if (input.cursor) {
filters.push(
or(
lt(schema.feedbacks.createdAt, input.cursor.createdAt),
and(
eq(schema.feedbacks.createdAt, input.cursor.createdAt),
lt(schema.feedbacks.id, input.cursor.id),
),
),
);
}

const rows = await database()
.select()
.from(schema.feedbacks)
.innerJoin(schema.slackUsers, eq(schema.slackUsers.id, schema.feedbacks.receiveSlackUserId))
.innerJoin(schema.users, eq(schema.users.id, schema.slackUsers.userId))
.where(and(...filters))
.orderBy(desc(schema.feedbacks.createdAt));

return rows.map((row) => ({
id: row.feedbacks.id,
content: row.feedbacks.content,
createdAt: row.feedbacks.createdAt,
cursor: {
id: row.feedbacks.id,
createdAt: row.feedbacks.createdAt,
},
}));
};

return await Promise.all(inputs.map((input) => execute(input)));
}, { cacheKeyFn: serialize }));

return ResultAsync.fromThrowable(
() => loader.load(input),
(error) => new UserReceivedFeedbacksUnexpectedError({ cause: error }),
)();
};

0 comments on commit 54985af

Please sign in to comment.