This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export * from './issue.type';
|
||||
export * from './ui';
|
||||
export * from './lib';
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { categorySchema } from '../category/category.schema';
|
||||
|
||||
export const issueSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string().trim().min(1).max(30),
|
||||
description: z.string().trim().max(50).nullable(),
|
||||
feedbackCount: z.number(),
|
||||
issueAdminUserId: z.number().nullable().optional(),
|
||||
status: z.union([
|
||||
z.literal('INIT'),
|
||||
z.literal('ON_REVIEW'),
|
||||
z.literal('DETAILED_REVIEW'),
|
||||
z.literal('IN_PROGRESS'),
|
||||
z.literal('RESOLVED'),
|
||||
z.literal('PENDING'),
|
||||
]),
|
||||
externalIssueId: z.string().trim().optional(),
|
||||
externalIssueUrl: z.string().url().nullable().optional(),
|
||||
externalIssueStatus: z.string().nullable().optional(),
|
||||
externalIssueSyncStatus: z.string().optional(),
|
||||
externalIssueSyncError: z.string().nullable().optional(),
|
||||
externalIssueSyncedAt: z.string().nullable().optional(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string().nullable(),
|
||||
category: categorySchema.nullable(),
|
||||
});
|
||||
|
||||
export const issueFormSchema = issueSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
status: true,
|
||||
externalIssueId: true,
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { z } from 'zod';
|
||||
|
||||
import type { issueFormSchema, issueSchema } from './issue.schema';
|
||||
|
||||
export type Issue = z.infer<typeof issueSchema>;
|
||||
export type IssueStatus = Issue['status'];
|
||||
export type IssueFormSchema = z.infer<typeof issueFormSchema>;
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { default as useIssueSearch } from './use-issue-search';
|
||||
export { default as useIssueSearchInfinite } from './use-issue-search-infinite';
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
|
||||
import type {
|
||||
OAIMutationResponse,
|
||||
OAIRequestBody,
|
||||
SearchQuery,
|
||||
} from '@/shared';
|
||||
import { client } from '@/shared';
|
||||
|
||||
type TData = OAIMutationResponse<
|
||||
'/api/admin/projects/{projectId}/issues/search',
|
||||
'post'
|
||||
>;
|
||||
|
||||
interface IBody
|
||||
extends Omit<
|
||||
OAIRequestBody<'/api/admin/projects/{projectId}/issues/search', 'post'>,
|
||||
'queries'
|
||||
> {
|
||||
queries: SearchQuery[];
|
||||
}
|
||||
|
||||
const useIssueSearchInfinite = (
|
||||
projectId: number,
|
||||
body: IBody = { limit: 20, page: 1, queries: [], sort: {} },
|
||||
) => {
|
||||
return useInfiniteQuery<TData>({
|
||||
queryKey: [
|
||||
'/api/admin/projects/{projectId}/issues/search',
|
||||
projectId,
|
||||
body,
|
||||
],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
const { data: result } = await client.post({
|
||||
path: '/api/admin/projects/{projectId}/issues/search',
|
||||
pathParams: { projectId },
|
||||
body: { ...body, page: pageParam as number },
|
||||
});
|
||||
return result;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (!lastPage) return undefined;
|
||||
if (lastPage.meta.currentPage < lastPage.meta.totalPages) {
|
||||
return lastPage.meta.currentPage + 1;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
initialPageParam: 1,
|
||||
// Kanban renders one query per status column. Keep the results warm so
|
||||
// opening an issue detail sheet does not refetch all status columns.
|
||||
staleTime: 30_000,
|
||||
refetchOnWindowFocus: false,
|
||||
// The placeholder below must always trigger a real fetch on first mount.
|
||||
initialDataUpdatedAt: 0,
|
||||
initialData: {
|
||||
pageParams: [],
|
||||
pages: [
|
||||
{
|
||||
items: [],
|
||||
meta: {
|
||||
currentPage: 1,
|
||||
totalPages: 0,
|
||||
totalItems: 0,
|
||||
itemCount: 0,
|
||||
itemsPerPage: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default useIssueSearchInfinite;
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type {
|
||||
OAIMutationResponse,
|
||||
OAIRequestBody,
|
||||
SearchQuery,
|
||||
} from '@/shared';
|
||||
import { client } from '@/shared';
|
||||
|
||||
type TData = OAIMutationResponse<
|
||||
'/api/admin/projects/{projectId}/issues/search',
|
||||
'post'
|
||||
>;
|
||||
|
||||
interface IBody
|
||||
extends Omit<
|
||||
OAIRequestBody<'/api/admin/projects/{projectId}/issues/search', 'post'>,
|
||||
'queries'
|
||||
> {
|
||||
queries: SearchQuery[];
|
||||
}
|
||||
|
||||
const useIssueSearch = (
|
||||
projectId: number,
|
||||
body: IBody = { limit: 10, page: 1, queries: [], sort: {} },
|
||||
options?: Omit<UseQueryOptions<TData>, 'queryKey' | 'queryFn'>,
|
||||
) => {
|
||||
return useQuery<TData>({
|
||||
queryKey: [
|
||||
'/api/admin/projects/{projectId}/issues/search',
|
||||
projectId,
|
||||
body,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const { data: result } = await client.post({
|
||||
path: '/api/admin/projects/{projectId}/issues/search',
|
||||
pathParams: { projectId },
|
||||
body,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export default useIssueSearch;
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { default as IssueBadge } from './issue-badge.ui';
|
||||
export { default as IssueSelectBox } from './issue-select-box.ui';
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import type { BadgeProps } from '@ufb/react';
|
||||
import { Badge } from '@ufb/react';
|
||||
|
||||
import { ISSUES } from '@/shared';
|
||||
import { BADGE_COLOR_MAP } from '@/shared/constants/color-map';
|
||||
import type { BadgeColor } from '@/shared/constants/color-map';
|
||||
|
||||
import type { Issue, IssueStatus } from '../issue.type';
|
||||
|
||||
const ISSUE_COLOR_MAP: Record<IssueStatus, BadgeColor> = {
|
||||
INIT: 'yellow',
|
||||
ON_REVIEW: 'green',
|
||||
DETAILED_REVIEW: 'orange',
|
||||
IN_PROGRESS: 'sky',
|
||||
RESOLVED: 'zinc',
|
||||
PENDING: 'indigo',
|
||||
};
|
||||
interface IProps extends Omit<BadgeProps, 'color'> {
|
||||
right?: React.ReactNode;
|
||||
status: Issue['status'];
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const IssueBadge: React.FC<IProps> = ({ name, status, right, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Badge {...props} className={BADGE_COLOR_MAP[ISSUE_COLOR_MAP[status]]}>
|
||||
{name ?? ISSUES(t).find((v) => v.key === status)?.name} {right}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
export default IssueBadge;
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useThrottle } from 'react-use';
|
||||
|
||||
import { client } from '@/shared';
|
||||
import AsyncMultiSelectSearchInput from '@/shared/ui/inputs/async-multi-select-search-input.ui';
|
||||
|
||||
import { useIssueSearchInfinite } from '../lib';
|
||||
|
||||
interface Props {
|
||||
onChange: (value?: number[]) => void;
|
||||
value?: number[];
|
||||
}
|
||||
|
||||
const IssueSelectBox = ({ onChange, value }: Props) => {
|
||||
const router = useRouter();
|
||||
const projectId = Number(router.query.projectId as string);
|
||||
const [selectedIssues, setSelectedIssues] = useState<
|
||||
{ label: string; value: string }[]
|
||||
>([]);
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const throttedValue = useThrottle(inputValue, 500);
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useIssueSearchInfinite(projectId, {
|
||||
queries: [{ key: 'name', value: throttedValue, condition: 'CONTAINS' }],
|
||||
sort: { name: 'ASC' },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setSelectedIssues([]);
|
||||
return;
|
||||
}
|
||||
void Promise.allSettled(
|
||||
value.map(async (issueId) => {
|
||||
try {
|
||||
const { data } = await client.get({
|
||||
path: '/api/admin/projects/{projectId}/issues/{issueId}',
|
||||
pathParams: { projectId, issueId },
|
||||
});
|
||||
return { label: data.name, value: String(issueId) };
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch issue ${issueId}:`, error);
|
||||
return { label: `Issue ${issueId}`, value: String(issueId) };
|
||||
}
|
||||
}),
|
||||
).then((results) => {
|
||||
const successfulResults = results
|
||||
.filter((result) => result.status === 'fulfilled')
|
||||
.map((result) => result.value);
|
||||
setSelectedIssues(successfulResults);
|
||||
});
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<AsyncMultiSelectSearchInput
|
||||
options={data.pages.flatMap((page) =>
|
||||
page ?
|
||||
page.items.map(({ name, id }) => ({ label: name, value: String(id) }))
|
||||
: [],
|
||||
)}
|
||||
value={selectedIssues}
|
||||
onChange={(input) => onChange(input.map((v) => Number(v.value)))}
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default IssueSelectBox;
|
||||
Reference in New Issue
Block a user