Files
Q-A_test/apps/web/src/entities/feedback/ui/feedback-cell.tsx
T
SDI 12e4f17b62
CI / typecheck (push) Successful in 1m8s
CI / format (push) Failing after 1m6s
CI / lint (push) Failing after 49s
CI / test (push) Failing after 1m8s
first commit
2026-07-15 18:05:12 +09:00

157 lines
4.8 KiB
TypeScript

/**
* 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 { memo } from 'react';
import dayjs from 'dayjs';
import Linkify from 'linkify-react';
import { Badge, Icon, Tag } from '@ufb/react';
import { DATE_TIME_FORMAT, ExpandableText, ImagePreviewButton } from '@/shared';
import { AICell } from '@/entities/ai';
import type { FieldInfo } from '@/entities/field';
import { useAIFIeldFeedbackCellLoading } from '..';
interface IProps {
isExpanded: boolean;
field: FieldInfo;
value: unknown;
feedbackId: number;
}
const toRenderableText = (value: unknown) => {
if (typeof value === 'string') return value;
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
if (value && typeof value === 'object') {
return JSON.stringify(value, null, 2);
}
return undefined;
};
const FeedbackCell: React.FC<IProps> = memo((props) => {
const { isExpanded, field, value, feedbackId } = props;
const renderableText = toRenderableText(value);
return (
<ExpandableText isExpanded={isExpanded}>
{typeof value === 'undefined' || value === null ?
undefined
: <>
{field.format === 'date' &&
dayjs(value as string).format(DATE_TIME_FORMAT)}
{field.format === 'multiSelect' && (
<div className="flex gap-2">
{(value as string[])
.sort(
(aKey, bKey) =>
(field.options ?? []).findIndex(
(option) => option.key === aKey,
) -
(field.options ?? []).findIndex(
(option) => option.key === bKey,
),
)
.map((key) => (
<Badge variant="subtle" key={key}>
{
(field.options?.find((option) => option.key === key)
?.name ?? value) as string
}
</Badge>
))}
</div>
)}
{field.format === 'select' && (
<Badge variant="subtle">
{field.options?.find((option) => option.key === value)?.name ??
''}
</Badge>
)}
{field.format === 'images' && (
<>
{Array.isArray(value) && (value as string[]).length > 0 ?
<ImagePreviewButton urls={value}>
<Tag
aria-label="View images"
variant="outline"
size="small"
className="cursor-pointer gap-1"
onClick={(e) => e.stopPropagation()}
>
<Icon name="RiImageFill" size={12} />
Image
</Tag>
</ImagePreviewButton>
: '-'}
</>
)}
{field.format === 'text' && (
<Linkify
options={{
className: 'text-blue-500 underline',
target: '_blank',
rel: 'noopener noreferrer',
attributes: {
onClick: (e: React.MouseEvent) => e.stopPropagation(),
},
}}
>
{renderableText}
</Linkify>
)}
{field.format === 'keyword' && renderableText}
{field.format === 'number' && renderableText}
{field.format === 'aiField' && (
<FeedbackAIFieldCell
value={
value as
| { status: 'loading' | 'success' | 'error'; message: string }
| undefined
}
feedbackId={feedbackId}
field={field}
/>
)}
</>
}
</ExpandableText>
);
});
const FeedbackAIFieldCell: React.FC<{
value?: { status: 'loading' | 'success' | 'error'; message: string };
feedbackId: number;
field: FieldInfo;
}> = memo((props) => {
const { field, value, feedbackId } = props;
const loadingFeedbackIds = useAIFIeldFeedbackCellLoading(
(state) => state.loadingFeedbackIds,
);
return (
<AICell
value={value}
isLoading={
loadingFeedbackIds.has(feedbackId) && field.status === 'ACTIVE'
}
/>
);
});
export default FeedbackCell;