Remove usage of tiley (#4406)
* First pass * Mooarrr * lint * snapshots
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { stringToColor } from "@shared/utils/color";
|
||||
import TeamNew from "~/scenes/TeamNew";
|
||||
import TeamLogo from "~/components/TeamLogo";
|
||||
import { createAction } from "~/actions";
|
||||
import { loadSessionsFromCookie } from "~/hooks/useSessions";
|
||||
import { TeamSection } from "../sections";
|
||||
@@ -11,7 +13,18 @@ export const switchTeamList = getSessions().map((session) => {
|
||||
name: session.name,
|
||||
section: TeamSection,
|
||||
keywords: "change switch workspace organization team",
|
||||
icon: () => <Logo alt={session.name} src={session.logoUrl} />,
|
||||
icon: () => (
|
||||
<StyledTeamLogo
|
||||
alt={session.name}
|
||||
model={{
|
||||
initial: session.name[0],
|
||||
avatarUrl: session.logoUrl,
|
||||
id: session.teamId,
|
||||
color: stringToColor(session.teamId),
|
||||
}}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
visible: ({ currentTeamId }) => currentTeamId !== session.teamId,
|
||||
perform: () => (window.location.href = session.url),
|
||||
});
|
||||
@@ -55,10 +68,9 @@ function getSessions(params?: { exclude?: string }) {
|
||||
return otherSessions;
|
||||
}
|
||||
|
||||
const Logo = styled("img")`
|
||||
const StyledTeamLogo = styled(TeamLogo)`
|
||||
border-radius: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 0;
|
||||
`;
|
||||
|
||||
export const rootTeamActions = [switchTeam, createTeam];
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import User from "~/models/User";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import Initials from "./Initials";
|
||||
import placeholder from "./placeholder.png";
|
||||
|
||||
export interface IAvatar {
|
||||
avatarUrl: string | null;
|
||||
color: string;
|
||||
initial: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
src: string;
|
||||
size: number;
|
||||
src?: string;
|
||||
icon?: React.ReactNode;
|
||||
user?: User;
|
||||
model?: IAvatar;
|
||||
alt?: string;
|
||||
showBorder?: boolean;
|
||||
onClick?: React.MouseEventHandler<HTMLImageElement>;
|
||||
@@ -16,20 +23,28 @@ type Props = {
|
||||
};
|
||||
|
||||
function Avatar(props: Props) {
|
||||
const { src, icon, showBorder, ...rest } = props;
|
||||
|
||||
const { icon, showBorder, model, ...rest } = props;
|
||||
const src = props.src || model?.avatarUrl;
|
||||
const [error, handleError] = useBoolean(false);
|
||||
|
||||
return (
|
||||
<AvatarWrapper>
|
||||
<CircleImg
|
||||
onError={handleError}
|
||||
src={error ? placeholder : src}
|
||||
$showBorder={showBorder}
|
||||
{...rest}
|
||||
/>
|
||||
<Relative>
|
||||
{src ? (
|
||||
<CircleImg
|
||||
onError={handleError}
|
||||
src={error ? placeholder : src}
|
||||
$showBorder={showBorder}
|
||||
{...rest}
|
||||
/>
|
||||
) : model ? (
|
||||
<Initials color={model.color} $showBorder={showBorder} {...rest}>
|
||||
{model.initial}
|
||||
</Initials>
|
||||
) : (
|
||||
<Initials $showBorder={showBorder} {...rest} />
|
||||
)}
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
</AvatarWrapper>
|
||||
</Relative>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +52,7 @@ Avatar.defaultProps = {
|
||||
size: 24,
|
||||
};
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ function AvatarWithPresence({
|
||||
$isObserving={isObserving}
|
||||
$color={user.color}
|
||||
>
|
||||
<Avatar src={user.avatarUrl} onClick={onClick} size={32} />
|
||||
<Avatar model={user} onClick={onClick} size={32} />
|
||||
</AvatarWrapper>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
27
app/components/Avatar/Initials.tsx
Normal file
27
app/components/Avatar/Initials.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import styled from "styled-components";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
const Initials = styled(Flex)<{
|
||||
color?: string;
|
||||
size: number;
|
||||
$showBorder?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
background-color: ${(props) => props.color};
|
||||
width: ${(props) => props.size}px;
|
||||
height: ${(props) => props.size}px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid
|
||||
${(props) =>
|
||||
props.$showBorder === false ? "transparent" : props.theme.background};
|
||||
flex-shrink: 0;
|
||||
font-size: ${(props) => props.size / 2}px;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export default Initials;
|
||||
@@ -43,10 +43,10 @@ function DocumentViews({ document, isOpen }: Props) {
|
||||
<PaginatedList
|
||||
aria-label={t("Viewers")}
|
||||
items={users}
|
||||
renderItem={(item: User) => {
|
||||
const view = documentViews.find((v) => v.user.id === item.id);
|
||||
const isPresent = presentIds.includes(item.id);
|
||||
const isEditing = editingIds.includes(item.id);
|
||||
renderItem={(model: User) => {
|
||||
const view = documentViews.find((v) => v.user.id === model.id);
|
||||
const isPresent = presentIds.includes(model.id);
|
||||
const isEditing = editingIds.includes(model.id);
|
||||
const subtitle = isPresent
|
||||
? isEditing
|
||||
? t("Currently editing")
|
||||
@@ -58,10 +58,10 @@ function DocumentViews({ document, isOpen }: Props) {
|
||||
});
|
||||
return (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
key={model.id}
|
||||
title={model.name}
|
||||
subtitle={subtitle}
|
||||
image={<Avatar key={item.id} src={item.avatarUrl} size={32} />}
|
||||
image={<Avatar key={model.id} model={model} size={32} />}
|
||||
border={false}
|
||||
small
|
||||
/>
|
||||
|
||||
@@ -142,7 +142,7 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
|
||||
onClick={handleTimeClick}
|
||||
/>
|
||||
}
|
||||
image={<Avatar src={event.actor?.avatarUrl} size={32} />}
|
||||
image={<Avatar model={event.actor} size={32} />}
|
||||
subtitle={
|
||||
<Subtitle>
|
||||
{icon}
|
||||
|
||||
@@ -39,7 +39,7 @@ function Facepile({
|
||||
}
|
||||
|
||||
function DefaultAvatar(user: User) {
|
||||
return <Avatar user={user} src={user.avatarUrl} size={32} />;
|
||||
return <Avatar model={user} size={32} />;
|
||||
}
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
|
||||
@@ -63,14 +63,7 @@ function AppSidebar() {
|
||||
<SidebarButton
|
||||
{...props}
|
||||
title={team.name}
|
||||
image={
|
||||
<StyledTeamLogo
|
||||
src={team.avatarUrl}
|
||||
width={32}
|
||||
height={32}
|
||||
alt={t("Logo")}
|
||||
/>
|
||||
}
|
||||
image={<TeamLogo model={team} size={32} alt={t("Logo")} />}
|
||||
showDisclosure
|
||||
/>
|
||||
)}
|
||||
@@ -139,11 +132,6 @@ function AppSidebar() {
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTeamLogo = styled(TeamLogo)`
|
||||
margin-right: 4px;
|
||||
background: white;
|
||||
`;
|
||||
|
||||
const Drafts = styled(Text)`
|
||||
margin: 0 4px;
|
||||
`;
|
||||
|
||||
@@ -178,7 +178,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(
|
||||
image={
|
||||
<StyledAvatar
|
||||
alt={user.name}
|
||||
src={user.avatarUrl}
|
||||
model={user}
|
||||
size={24}
|
||||
showBorder={false}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import styled from "styled-components";
|
||||
import Avatar from "./Avatar";
|
||||
|
||||
const TeamLogo = styled.img<{ width?: number; height?: number; size?: string }>`
|
||||
width: ${(props) =>
|
||||
props.width ? `${props.width}px` : props.size || "auto"};
|
||||
height: ${(props) =>
|
||||
props.height ? `${props.height}px` : props.size || "38px"};
|
||||
const TeamLogo = styled(Avatar)`
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${(props) => props.theme.divider};
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { computed, observable } from "mobx";
|
||||
import { TeamPreference, TeamPreferences } from "@shared/types";
|
||||
import { stringToColor } from "@shared/utils/color";
|
||||
import BaseModel from "./BaseModel";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
@@ -69,6 +70,16 @@ class Team extends BaseModel {
|
||||
return "SSO";
|
||||
}
|
||||
|
||||
@computed
|
||||
get color(): string {
|
||||
return stringToColor(this.id);
|
||||
}
|
||||
|
||||
@computed
|
||||
get initial(): string {
|
||||
return this.name ? this.name[0] : "?";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this team is using a separate editing mode behind an "Edit"
|
||||
* button rather than seamless always-editing.
|
||||
|
||||
@@ -40,6 +40,11 @@ class User extends ParanoidModel {
|
||||
|
||||
isSuspended: boolean;
|
||||
|
||||
@computed
|
||||
get initial(): string {
|
||||
return this.name ? this.name[0] : "?";
|
||||
}
|
||||
|
||||
@computed
|
||||
get isInvited(): boolean {
|
||||
return !this.lastActiveAt;
|
||||
|
||||
@@ -104,18 +104,16 @@ const MembershipPreview = ({ collection, limit = 8 }: Props) => {
|
||||
users={sortBy(collectionUsers, "lastActiveAt")}
|
||||
overflow={overflow}
|
||||
limit={limit}
|
||||
renderAvatar={(user) => (
|
||||
<StyledAvatar user={user} src={user.avatarUrl} size={32} />
|
||||
)}
|
||||
renderAvatar={(user) => <StyledAvatar model={user} size={32} />}
|
||||
/>
|
||||
</Fade>
|
||||
</NudeButton>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledAvatar = styled(Avatar)<{ user: User }>`
|
||||
const StyledAvatar = styled(Avatar)<{ model: User }>`
|
||||
transition: opacity 250ms ease-in-out;
|
||||
opacity: ${(props) => (props.user.isRecentlyActive ? 1 : 0.5)};
|
||||
opacity: ${(props) => (props.model.isRecentlyActive ? 1 : 0.5)};
|
||||
`;
|
||||
|
||||
export default observer(MembershipPreview);
|
||||
|
||||
@@ -49,7 +49,7 @@ const MemberListItem = ({
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
|
||||
</>
|
||||
}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
image={<Avatar model={user} size={32} />}
|
||||
actions={
|
||||
<Flex align="center" gap={8}>
|
||||
{onUpdate && (
|
||||
|
||||
@@ -21,7 +21,7 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
|
||||
return (
|
||||
<ListItem
|
||||
title={user.name}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
image={<Avatar model={user} size={32} />}
|
||||
subtitle={
|
||||
<>
|
||||
{user.lastActiveAt ? (
|
||||
|
||||
@@ -35,7 +35,7 @@ const GroupMemberListItem = ({ user, onRemove, onAdd }: Props) => {
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
|
||||
</>
|
||||
}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
image={<Avatar model={user} size={32} />}
|
||||
actions={
|
||||
<Flex align="center">
|
||||
{onRemove && <GroupMemberMenu onRemove={onRemove} />}
|
||||
|
||||
@@ -21,7 +21,7 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
|
||||
return (
|
||||
<ListItem
|
||||
title={user.name}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
image={<Avatar model={user} size={32} />}
|
||||
subtitle={
|
||||
<>
|
||||
{user.lastActiveAt ? (
|
||||
|
||||
@@ -169,7 +169,7 @@ function Login({ children }: Props) {
|
||||
/>
|
||||
<Logo>
|
||||
{config.logo ? (
|
||||
<TeamLogo width={48} height={48} src={config.logo} />
|
||||
<TeamLogo size={48} src={config.logo} />
|
||||
) : (
|
||||
<OutlineLogo size={42} fill="currentColor" />
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,6 @@ function Details() {
|
||||
const form = useRef<HTMLFormElement>(null);
|
||||
const [name, setName] = useState(team.name);
|
||||
const [subdomain, setSubdomain] = useState(team.subdomain);
|
||||
const [avatarUrl, setAvatarUrl] = useState<string>(team.avatarUrl);
|
||||
const [defaultCollectionId, setDefaultCollectionId] = useState<string | null>(
|
||||
team.defaultCollectionId
|
||||
);
|
||||
@@ -40,7 +39,6 @@ function Details() {
|
||||
try {
|
||||
await auth.updateTeam({
|
||||
name,
|
||||
avatarUrl,
|
||||
subdomain,
|
||||
defaultCollectionId,
|
||||
});
|
||||
@@ -53,7 +51,7 @@ function Details() {
|
||||
});
|
||||
}
|
||||
},
|
||||
[auth, name, avatarUrl, subdomain, defaultCollectionId, showToast, t]
|
||||
[auth, name, subdomain, defaultCollectionId, showToast, t]
|
||||
);
|
||||
|
||||
const handleNameChange = React.useCallback(
|
||||
@@ -71,7 +69,6 @@ function Details() {
|
||||
);
|
||||
|
||||
const handleAvatarUpload = async (avatarUrl: string) => {
|
||||
setAvatarUrl(avatarUrl);
|
||||
await auth.updateTeam({
|
||||
avatarUrl,
|
||||
});
|
||||
@@ -115,7 +112,7 @@ function Details() {
|
||||
<ImageInput
|
||||
onSuccess={handleAvatarUpload}
|
||||
onError={handleAvatarError}
|
||||
src={avatarUrl}
|
||||
model={team}
|
||||
borderRadius={0}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
@@ -18,7 +18,6 @@ const Profile = () => {
|
||||
const user = useCurrentUser();
|
||||
const form = React.useRef<HTMLFormElement>(null);
|
||||
const [name, setName] = React.useState<string>(user.name || "");
|
||||
const [avatarUrl, setAvatarUrl] = React.useState<string>(user.avatarUrl);
|
||||
const { showToast } = useToasts();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -28,7 +27,6 @@ const Profile = () => {
|
||||
try {
|
||||
await auth.updateUser({
|
||||
name,
|
||||
avatarUrl,
|
||||
});
|
||||
showToast(t("Profile saved"), {
|
||||
type: "success",
|
||||
@@ -45,7 +43,6 @@ const Profile = () => {
|
||||
};
|
||||
|
||||
const handleAvatarUpload = async (avatarUrl: string) => {
|
||||
setAvatarUrl(avatarUrl);
|
||||
await auth.updateUser({
|
||||
avatarUrl,
|
||||
});
|
||||
@@ -79,7 +76,7 @@ const Profile = () => {
|
||||
<ImageInput
|
||||
onSuccess={handleAvatarUpload}
|
||||
onError={handleAvatarError}
|
||||
src={avatarUrl}
|
||||
model={user}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Avatar, { IAvatar } from "~/components/Avatar/Avatar";
|
||||
import Flex from "~/components/Flex";
|
||||
import ImageUpload, { Props as ImageUploadProps } from "./ImageUpload";
|
||||
|
||||
type Props = ImageUploadProps & {
|
||||
src?: string;
|
||||
model: IAvatar;
|
||||
};
|
||||
|
||||
export default function ImageInput({ src, ...rest }: Props) {
|
||||
export default function ImageInput({ model, ...rest }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ImageBox>
|
||||
<ImageUpload {...rest}>
|
||||
<Avatar src={src} />
|
||||
<Flex auto align="center" justify="center">
|
||||
<StyledAvatar model={model} size={64} />
|
||||
<Flex auto align="center" justify="center" className="upload">
|
||||
{t("Upload")}
|
||||
</Flex>
|
||||
</ImageUpload>
|
||||
@@ -28,8 +29,8 @@ const avatarStyles = `
|
||||
height: 64px;
|
||||
`;
|
||||
|
||||
const Avatar = styled.img`
|
||||
${avatarStyles};
|
||||
const StyledAvatar = styled(Avatar)`
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
const ImageBox = styled(Flex)`
|
||||
@@ -41,7 +42,7 @@ const ImageBox = styled(Flex)`
|
||||
background: ${(props) => props.theme.background};
|
||||
overflow: hidden;
|
||||
|
||||
div div {
|
||||
.upload {
|
||||
${avatarStyles};
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -53,7 +54,7 @@ const ImageBox = styled(Flex)`
|
||||
transition: all 250ms;
|
||||
}
|
||||
|
||||
&:hover div {
|
||||
&:hover .upload {
|
||||
opacity: 1;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
color: ${(props) => props.theme.white};
|
||||
|
||||
@@ -29,7 +29,7 @@ function PeopleTable({ canManage, ...rest }: Props) {
|
||||
Cell: observer(
|
||||
({ value, row }: { value: string; row: { original: User } }) => (
|
||||
<Flex align="center" gap={8}>
|
||||
<Avatar src={row.original.avatarUrl} size={32} /> {value}{" "}
|
||||
<Avatar model={row.original} size={32} /> {value}{" "}
|
||||
{currentUser.id === row.original.id && `(${t("You")})`}
|
||||
</Flex>
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ function SharesTable({ canManage, ...rest }: Props) {
|
||||
<Flex align="center" gap={4}>
|
||||
{row.original.createdBy && (
|
||||
<Avatar
|
||||
src={row.original.createdBy.avatarUrl}
|
||||
model={row.original.createdBy}
|
||||
alt={row.original.createdBy.name}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -20,7 +20,7 @@ const UserListItem = ({ user, showMenu }: Props) => {
|
||||
return (
|
||||
<ListItem
|
||||
title={<Title>{user.name}</Title>}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
image={<Avatar model={user} size={32} />}
|
||||
subtitle={
|
||||
<>
|
||||
{user.email ? `${user.email} · ` : undefined}
|
||||
|
||||
@@ -39,7 +39,7 @@ function UserProfile(props: Props) {
|
||||
<Modal
|
||||
title={
|
||||
<Flex align="center">
|
||||
<Avatar src={user.avatarUrl} size={38} alt={t("Profile picture")} />
|
||||
<Avatar model={user} size={38} alt={t("Profile picture")} />
|
||||
<span> {user.name}</span>
|
||||
</Flex>
|
||||
}
|
||||
|
||||
@@ -226,14 +226,17 @@ export default class AuthStore {
|
||||
preferences?: UserPreferences;
|
||||
}) => {
|
||||
this.isSaving = true;
|
||||
const previousData = this.user?.toAPI();
|
||||
|
||||
try {
|
||||
this.user?.updateFromJson(params);
|
||||
const res = await client.post(`/users.update`, params);
|
||||
invariant(res?.data, "User response not available");
|
||||
runInAction("AuthStore#updateUser", () => {
|
||||
this.addPolicies(res.policies);
|
||||
this.user = new User(res.data, this);
|
||||
});
|
||||
this.user?.updateFromJson(res.data);
|
||||
this.addPolicies(res.policies);
|
||||
} catch (err) {
|
||||
this.user?.updateFromJson(previousData);
|
||||
throw err;
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
@@ -251,14 +254,17 @@ export default class AuthStore {
|
||||
preferences?: TeamPreferences;
|
||||
}) => {
|
||||
this.isSaving = true;
|
||||
const previousData = this.team?.toAPI();
|
||||
|
||||
try {
|
||||
this.team?.updateFromJson(params);
|
||||
const res = await client.post(`/team.update`, params);
|
||||
invariant(res?.data, "Team response not available");
|
||||
runInAction("AuthStore#updateTeam", () => {
|
||||
this.addPolicies(res.policies);
|
||||
this.team = new Team(res.data, this);
|
||||
});
|
||||
this.team?.updateFromJson(res.data);
|
||||
this.addPolicies(res.policies);
|
||||
} catch (err) {
|
||||
this.team?.updateFromJson(previousData);
|
||||
throw err;
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user