Move distinct components to individual files
This commit is contained in:
parent
97f703f7d7
commit
834e36b5a3
6 changed files with 157 additions and 98 deletions
17
src/apps/experimental/components/GridActionsCellLink.tsx
Normal file
17
src/apps/experimental/components/GridActionsCellLink.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React, { type RefAttributes } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { GridActionsCellItem, type GridActionsCellItemProps } from '@mui/x-data-grid';
|
||||||
|
|
||||||
|
type GridActionsCellLinkProps = { to: string } & GridActionsCellItemProps & RefAttributes<HTMLButtonElement>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link component to use in mui's data-grid action column due to a current bug with passing props to custom link components.
|
||||||
|
* @see https://github.com/mui/mui-x/issues/4654
|
||||||
|
*/
|
||||||
|
const GridActionsCellLink = ({ to, ...props }: GridActionsCellLinkProps) => (
|
||||||
|
<Link to={to}>
|
||||||
|
<GridActionsCellItem {...props} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default GridActionsCellLink;
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
const LogLevelChip = ({ level }: { level: LogLevel }) => {
|
||||||
|
let color: 'info' | 'warning' | 'error' | undefined = undefined;
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.Information:
|
||||||
|
color = 'info';
|
||||||
|
break;
|
||||||
|
case LogLevel.Warning:
|
||||||
|
color = 'warning';
|
||||||
|
break;
|
||||||
|
case LogLevel.Error:
|
||||||
|
case LogLevel.Critical:
|
||||||
|
color = 'error';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelText = globalize.translate(`LogLevel.${level}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
size='small'
|
||||||
|
color={color}
|
||||||
|
label={levelText}
|
||||||
|
title={levelText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogLevelChip;
|
|
@ -0,0 +1,64 @@
|
||||||
|
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||||
|
import Info from '@mui/icons-material/Info';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import React, { FC, useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
const OverviewCell: FC<ActivityLogEntry> = ({ Overview, ShortOverview }) => {
|
||||||
|
const displayValue = ShortOverview ?? Overview;
|
||||||
|
const [ open, setOpen ] = useState(false);
|
||||||
|
|
||||||
|
const onTooltipClose = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onTooltipOpen = useCallback(() => {
|
||||||
|
setOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!displayValue) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis'
|
||||||
|
}}
|
||||||
|
component='div'
|
||||||
|
title={displayValue}
|
||||||
|
>
|
||||||
|
{displayValue}
|
||||||
|
</Box>
|
||||||
|
{ShortOverview && Overview && (
|
||||||
|
<ClickAwayListener onClickAway={onTooltipClose}>
|
||||||
|
<Tooltip
|
||||||
|
title={Overview}
|
||||||
|
placement='top'
|
||||||
|
arrow
|
||||||
|
onClose={onTooltipClose}
|
||||||
|
open={open}
|
||||||
|
disableFocusListener
|
||||||
|
disableHoverListener
|
||||||
|
disableTouchListener
|
||||||
|
>
|
||||||
|
<IconButton onClick={onTooltipOpen}>
|
||||||
|
<Info />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ClickAwayListener>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OverviewCell;
|
|
@ -1,20 +1,16 @@
|
||||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api';
|
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api';
|
||||||
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
||||||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||||
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
|
||||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||||
import Info from '@mui/icons-material/Info';
|
import PermMedia from '@mui/icons-material/PermMedia';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Chip from '@mui/material/Chip';
|
|
||||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import ToggleButton from '@mui/material/ToggleButton';
|
import ToggleButton from '@mui/material/ToggleButton';
|
||||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import Page from 'components/Page';
|
import Page from 'components/Page';
|
||||||
import UserAvatar from 'components/UserAvatar';
|
import UserAvatar from 'components/UserAvatar';
|
||||||
|
@ -23,6 +19,10 @@ import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'script
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
import { toBoolean } from 'utils/string';
|
import { toBoolean } from 'utils/string';
|
||||||
|
|
||||||
|
import LogLevelChip from '../../components/activityTable/LogLevelChip';
|
||||||
|
import OverviewCell from '../../components/activityTable/OverviewCell';
|
||||||
|
import GridActionsCellLink from '../../components/GridActionsCellLink';
|
||||||
|
|
||||||
const DEFAULT_PAGE_SIZE = 25;
|
const DEFAULT_PAGE_SIZE = 25;
|
||||||
const VIEW_PARAM = 'useractivity';
|
const VIEW_PARAM = 'useractivity';
|
||||||
|
|
||||||
|
@ -40,88 +40,6 @@ const getActivityView = (param: string | null) => {
|
||||||
|
|
||||||
const getRowId = (row: ActivityLogEntry) => row.Id ?? -1;
|
const getRowId = (row: ActivityLogEntry) => row.Id ?? -1;
|
||||||
|
|
||||||
const LogLevelChip = ({ level }: { level: LogLevel }) => {
|
|
||||||
let color: 'info' | 'warning' | 'error' | undefined = undefined;
|
|
||||||
switch (level) {
|
|
||||||
case LogLevel.Information:
|
|
||||||
color = 'info';
|
|
||||||
break;
|
|
||||||
case LogLevel.Warning:
|
|
||||||
color = 'warning';
|
|
||||||
break;
|
|
||||||
case LogLevel.Error:
|
|
||||||
case LogLevel.Critical:
|
|
||||||
color = 'error';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const levelText = globalize.translate(`LogLevel.${level}`);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Chip
|
|
||||||
size='small'
|
|
||||||
color={color}
|
|
||||||
label={levelText}
|
|
||||||
title={levelText}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const OverviewCell: FC<ActivityLogEntry> = ({ Overview, ShortOverview }) => {
|
|
||||||
const displayValue = ShortOverview ?? Overview;
|
|
||||||
const [ open, setOpen ] = useState(false);
|
|
||||||
|
|
||||||
const onTooltipClose = useCallback(() => {
|
|
||||||
setOpen(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onTooltipOpen = useCallback(() => {
|
|
||||||
setOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!displayValue) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
width: '100%',
|
|
||||||
alignItems: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flexGrow: 1,
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis'
|
|
||||||
}}
|
|
||||||
component='div'
|
|
||||||
title={displayValue}
|
|
||||||
>
|
|
||||||
{displayValue}
|
|
||||||
</Box>
|
|
||||||
{ShortOverview && Overview && (
|
|
||||||
<ClickAwayListener onClickAway={onTooltipClose}>
|
|
||||||
<Tooltip
|
|
||||||
title={Overview}
|
|
||||||
placement='top'
|
|
||||||
arrow
|
|
||||||
onClose={onTooltipClose}
|
|
||||||
open={open}
|
|
||||||
disableFocusListener
|
|
||||||
disableHoverListener
|
|
||||||
disableTouchListener
|
|
||||||
>
|
|
||||||
<IconButton onClick={onTooltipOpen}>
|
|
||||||
<Info />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ClickAwayListener>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Activity = () => {
|
const Activity = () => {
|
||||||
const { api } = useApi();
|
const { api } = useApi();
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
@ -144,10 +62,16 @@ const Activity = () => {
|
||||||
width: 60,
|
width: 60,
|
||||||
valueGetter: ({ row }) => users[row.UserId]?.Name,
|
valueGetter: ({ row }) => users[row.UserId]?.Name,
|
||||||
renderCell: ({ row }) => (
|
renderCell: ({ row }) => (
|
||||||
<UserAvatar
|
<IconButton
|
||||||
user={users[row.UserId]}
|
size='large'
|
||||||
showTitle
|
color='inherit'
|
||||||
/>
|
sx={{ padding: 0 }}
|
||||||
|
title={users[row.UserId]?.Name ?? undefined}
|
||||||
|
component={Link}
|
||||||
|
to={`/useredit.html?userId=${row.UserId}`}
|
||||||
|
>
|
||||||
|
<UserAvatar user={users[row.UserId]} />
|
||||||
|
</IconButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
] : [];
|
] : [];
|
||||||
|
@ -188,7 +112,7 @@ const Activity = () => {
|
||||||
{
|
{
|
||||||
field: 'Overview',
|
field: 'Overview',
|
||||||
headerName: globalize.translate('LabelOverview'),
|
headerName: globalize.translate('LabelOverview'),
|
||||||
width: 220,
|
width: 200,
|
||||||
valueGetter: ({ row }) => row.ShortOverview ?? row.Overview,
|
valueGetter: ({ row }) => row.ShortOverview ?? row.Overview,
|
||||||
renderCell: ({ row }) => (
|
renderCell: ({ row }) => (
|
||||||
<OverviewCell {...row} />
|
<OverviewCell {...row} />
|
||||||
|
@ -197,7 +121,28 @@ const Activity = () => {
|
||||||
{
|
{
|
||||||
field: 'Type',
|
field: 'Type',
|
||||||
headerName: globalize.translate('LabelType'),
|
headerName: globalize.translate('LabelType'),
|
||||||
width: 150
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
type: 'actions',
|
||||||
|
getActions: ({ row }) => {
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
if (row.ItemId) {
|
||||||
|
actions.push(
|
||||||
|
<GridActionsCellLink
|
||||||
|
size='large'
|
||||||
|
icon={<PermMedia />}
|
||||||
|
label={globalize.translate('LabelMediaDetails')}
|
||||||
|
title={globalize.translate('LabelMediaDetails')}
|
||||||
|
to={`/details?id=${row.ItemId}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -7,17 +7,15 @@ import { useApi } from 'hooks/useApi';
|
||||||
|
|
||||||
interface UserAvatarProps {
|
interface UserAvatarProps {
|
||||||
user?: UserDto
|
user?: UserDto
|
||||||
showTitle?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserAvatar: FC<UserAvatarProps> = ({ user, showTitle = false }) => {
|
const UserAvatar: FC<UserAvatarProps> = ({ user }) => {
|
||||||
const { api } = useApi();
|
const { api } = useApi();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return user ? (
|
return user ? (
|
||||||
<Avatar
|
<Avatar
|
||||||
alt={user.Name ?? undefined}
|
alt={user.Name ?? undefined}
|
||||||
title={showTitle && user.Name ? user.Name : undefined}
|
|
||||||
src={
|
src={
|
||||||
api && user.Id && user.PrimaryImageTag ?
|
api && user.Id && user.PrimaryImageTag ?
|
||||||
`${api.basePath}/Users/${user.Id}/Images/Primary?tag=${user.PrimaryImageTag}` :
|
`${api.basePath}/Users/${user.Id}/Images/Primary?tag=${user.PrimaryImageTag}` :
|
||||||
|
|
|
@ -730,6 +730,7 @@
|
||||||
"LabelMaxDaysForNextUp": "Max days in 'Next Up'",
|
"LabelMaxDaysForNextUp": "Max days in 'Next Up'",
|
||||||
"LabelMaxDaysForNextUpHelp": "Set the maximum amount of days a show should stay in the 'Next Up' list without watching it.",
|
"LabelMaxDaysForNextUpHelp": "Set the maximum amount of days a show should stay in the 'Next Up' list without watching it.",
|
||||||
"LabelMaxVideoResolution": "Maximum Allowed Video Transcoding Resolution",
|
"LabelMaxVideoResolution": "Maximum Allowed Video Transcoding Resolution",
|
||||||
|
"LabelMediaDetails": "Media details",
|
||||||
"LabelLineup": "Lineup",
|
"LabelLineup": "Lineup",
|
||||||
"LabelLocalCustomCss": "Custom CSS code for styling which applies to this client only. You may want to disable server custom CSS code.",
|
"LabelLocalCustomCss": "Custom CSS code for styling which applies to this client only. You may want to disable server custom CSS code.",
|
||||||
"LabelLocalHttpServerPortNumber": "Local HTTP port number",
|
"LabelLocalHttpServerPortNumber": "Local HTTP port number",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue