mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0904b6a6a1 | |||
| 69995b5f9e | |||
| e04c77dc25 | |||
| 41028eda4d |
@@ -74,7 +74,9 @@ function DocumentListItem(props: Props) {
|
||||
<Heading>
|
||||
<Title text={document.titleWithDefault} highlight={highlight} />
|
||||
{document.isNew && document.createdBy.id !== currentUser.id && (
|
||||
<Badge yellow>{t("New")}</Badge>
|
||||
<Badge spaced yellow>
|
||||
{t("New")}
|
||||
</Badge>
|
||||
)}
|
||||
{canStar && (
|
||||
<StarPositioner>
|
||||
@@ -87,11 +89,13 @@ function DocumentListItem(props: Props) {
|
||||
delay={500}
|
||||
placement="top"
|
||||
>
|
||||
<Badge>{t("Draft")}</Badge>
|
||||
<Badge spaced>{t("Draft")}</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
{document.isTemplate && showTemplate && (
|
||||
<Badge primary>{t("Template")}</Badge>
|
||||
<Badge spaced primary>
|
||||
{t("Template")}
|
||||
</Badge>
|
||||
)}
|
||||
</Heading>
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import useStickyState from "../hooks/useStickyState";
|
||||
|
||||
type Props = {|
|
||||
children: React.Node,
|
||||
|};
|
||||
|
||||
export default function Session({ children }: Props) {
|
||||
const ref = React.useRef(false);
|
||||
const [, setPreviousSession] = useStickyState<string>("", "previous-session");
|
||||
const [currentSession, setCurrentSession] = useStickyState<string>(
|
||||
"",
|
||||
"current-session"
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!ref.current) {
|
||||
if (currentSession) {
|
||||
setPreviousSession(currentSession);
|
||||
}
|
||||
setCurrentSession(new Date().toISOString());
|
||||
ref.current = true;
|
||||
}
|
||||
}, [setCurrentSession, setPreviousSession, currentSession]);
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// @flow
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export default function useQuery() {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
export default function useStickyState<T>(
|
||||
defaultValue: ?T,
|
||||
key: string
|
||||
): [?T, (T) => void] {
|
||||
const [value, setValue] = React.useState<?T>(() => {
|
||||
const stickyValue = window.localStorage.getItem(key);
|
||||
return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue;
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
}, [key, value]);
|
||||
|
||||
return [value, setValue];
|
||||
}
|
||||
+39
-37
@@ -12,11 +12,11 @@ import Search from "scenes/Search";
|
||||
import Starred from "scenes/Starred";
|
||||
import Templates from "scenes/Templates";
|
||||
import Trash from "scenes/Trash";
|
||||
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import Layout from "components/Layout";
|
||||
import LoadingPlaceholder from "components/LoadingPlaceholder";
|
||||
import Route from "components/ProfiledRoute";
|
||||
import Session from "components/Session";
|
||||
import SocketProvider from "components/SocketProvider";
|
||||
import { matchDocumentSlug as slug } from "utils/routeHelpers";
|
||||
|
||||
@@ -35,42 +35,44 @@ export default function AuthenticatedRoutes() {
|
||||
return (
|
||||
<SocketProvider>
|
||||
<Layout>
|
||||
<Switch>
|
||||
<Redirect from="/dashboard" to="/home" />
|
||||
<Route path="/home/:tab" component={Home} />
|
||||
<Route path="/home" component={Home} />
|
||||
<Route exact path="/starred" component={Starred} />
|
||||
<Route exact path="/starred/:sort" component={Starred} />
|
||||
<Route exact path="/templates" component={Templates} />
|
||||
<Route exact path="/templates/:sort" component={Templates} />
|
||||
<Route exact path="/drafts" component={Drafts} />
|
||||
<Route exact path="/archive" component={Archive} />
|
||||
<Route exact path="/trash" component={Trash} />
|
||||
<Route exact path="/collections/:id/new" component={DocumentNew} />
|
||||
<Route exact path="/collections/:id/:tab" component={Collection} />
|
||||
<Route exact path="/collections/:id" component={Collection} />
|
||||
<Route exact path={`/d/${slug}`} component={RedirectDocument} />
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${slug}/history/:revisionId?`}
|
||||
component={KeyedDocument}
|
||||
/>
|
||||
<Route exact path={`/doc/${slug}/edit`} component={KeyedDocument} />
|
||||
<Route path={`/doc/${slug}`} component={KeyedDocument} />
|
||||
<Route exact path="/search" component={Search} />
|
||||
<Route exact path="/search/:term" component={Search} />
|
||||
<Route path="/404" component={Error404} />
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<CenteredContent>
|
||||
<LoadingPlaceholder />
|
||||
</CenteredContent>
|
||||
}
|
||||
>
|
||||
<SettingsRoutes />
|
||||
</React.Suspense>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
<Session>
|
||||
<Switch>
|
||||
<Redirect from="/dashboard" to="/home" />
|
||||
<Route path="/home/:tab" component={Home} />
|
||||
<Route path="/home" component={Home} />
|
||||
<Route exact path="/starred" component={Starred} />
|
||||
<Route exact path="/starred/:sort" component={Starred} />
|
||||
<Route exact path="/templates" component={Templates} />
|
||||
<Route exact path="/templates/:sort" component={Templates} />
|
||||
<Route exact path="/drafts" component={Drafts} />
|
||||
<Route exact path="/archive" component={Archive} />
|
||||
<Route exact path="/trash" component={Trash} />
|
||||
<Route exact path="/collections/:id/new" component={DocumentNew} />
|
||||
<Route exact path="/collections/:id/:tab" component={Collection} />
|
||||
<Route exact path="/collections/:id" component={Collection} />
|
||||
<Route exact path={`/d/${slug}`} component={RedirectDocument} />
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${slug}/history/:revisionId?`}
|
||||
component={KeyedDocument}
|
||||
/>
|
||||
<Route exact path={`/doc/${slug}/edit`} component={KeyedDocument} />
|
||||
<Route path={`/doc/${slug}`} component={KeyedDocument} />
|
||||
<Route exact path="/search" component={Search} />
|
||||
<Route exact path="/search/:term" component={Search} />
|
||||
<Route path="/404" component={Error404} />
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<CenteredContent>
|
||||
<LoadingPlaceholder />
|
||||
</CenteredContent>
|
||||
}
|
||||
>
|
||||
<SettingsRoutes />
|
||||
</React.Suspense>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</Session>
|
||||
</Layout>
|
||||
</SocketProvider>
|
||||
);
|
||||
|
||||
@@ -52,8 +52,12 @@ const MemberListItem = ({
|
||||
) : (
|
||||
t("Never signed in")
|
||||
)}
|
||||
{user.isInvited && <Badge>{t("Invited")}</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
|
||||
{user.isInvited && <Badge spaced>{t("Invited")}</Badge>}
|
||||
{user.isAdmin && (
|
||||
<Badge spaced primary={user.isAdmin}>
|
||||
{t("Admin")}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
|
||||
@@ -31,8 +31,12 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
|
||||
) : (
|
||||
t("Never signed in")
|
||||
)}
|
||||
{user.isInvited && <Badge>{t("Invited")}</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
|
||||
{user.isInvited && <Badge spaced>{t("Invited")}</Badge>}
|
||||
{user.isAdmin && (
|
||||
<Badge spaced primary={user.isAdmin}>
|
||||
{t("Admin")}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
|
||||
@@ -165,7 +165,7 @@ function DocumentHeader({
|
||||
title={
|
||||
<>
|
||||
{document.title}{" "}
|
||||
{document.isArchived && <Badge>{t("Archived")}</Badge>}
|
||||
{document.isArchived && <Badge spaced>{t("Archived")}</Badge>}
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
|
||||
@@ -28,8 +28,12 @@ const GroupMemberListItem = ({ user, onRemove, onAdd }: Props) => {
|
||||
) : (
|
||||
"Never signed in"
|
||||
)}
|
||||
{user.isInvited && <Badge>Invited</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
|
||||
{user.isInvited && <Badge spaced>Invited</Badge>}
|
||||
{user.isAdmin && (
|
||||
<Badge spaced primary={user.isAdmin}>
|
||||
Admin
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
|
||||
@@ -28,8 +28,12 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
|
||||
) : (
|
||||
"Never signed in"
|
||||
)}
|
||||
{user.isInvited && <Badge>Invited</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
|
||||
{user.isInvited && <Badge spaced>Invited</Badge>}
|
||||
{user.isAdmin && (
|
||||
<Badge spaced primary={user.isAdmin}>
|
||||
Admin
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
|
||||
+85
-10
@@ -2,25 +2,58 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { HomeIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { Switch, Route } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { Action } from "components/Actions";
|
||||
import Heading from "components/Heading";
|
||||
import HelpText from "components/HelpText";
|
||||
import InputSearch from "components/InputSearch";
|
||||
import LanguagePrompt from "components/LanguagePrompt";
|
||||
import Scene from "components/Scene";
|
||||
import Tab from "components/Tab";
|
||||
import Tabs from "components/Tabs";
|
||||
import PaginatedDocumentList from "../components/PaginatedDocumentList";
|
||||
import useStores from "../hooks/useStores";
|
||||
import useCurrentUser from "hooks/useCurrentUser";
|
||||
import useQuery from "hooks/useQuery";
|
||||
import useStickyState from "hooks/useStickyState";
|
||||
import useStores from "hooks/useStores";
|
||||
import NewDocumentMenu from "menus/NewDocumentMenu";
|
||||
|
||||
function Home() {
|
||||
const { documents, ui, auth } = useStores();
|
||||
function useWelcomeMessage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!auth.user || !auth.team) return null;
|
||||
const user = auth.user.id;
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 17) {
|
||||
return t("Good evening");
|
||||
}
|
||||
if (hour >= 12) {
|
||||
return t("Good afternoon");
|
||||
}
|
||||
return t("Good morning");
|
||||
}
|
||||
|
||||
function Home() {
|
||||
const [previousSession] = useStickyState<string>("", "previous-session");
|
||||
const welcomeMessage = useWelcomeMessage();
|
||||
const query = useQuery();
|
||||
const user = useCurrentUser();
|
||||
const { documents, ui } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const userId = user.id;
|
||||
|
||||
// see authentication middleware for where these query params are defined:
|
||||
// https://github.com/outline/outline/blob/ed2a42ac279e0ae23abcf846b621cf6bcf03e75f/server/middlewares/authentication.js#L165
|
||||
const teamOnboarding = query.get("newTeam") !== null;
|
||||
const userOnboarding = !teamOnboarding && query.get("newUser") !== null;
|
||||
const onboarding = teamOnboarding || userOnboarding;
|
||||
const showLanguagePrompt = !onboarding && !ui.languagePromptDismissed;
|
||||
|
||||
console.log({ showLanguagePrompt, teamOnboarding, userOnboarding });
|
||||
|
||||
const recent = previousSession
|
||||
? documents.updatedSinceTimestamp(previousSession)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Scene
|
||||
@@ -41,8 +74,45 @@ function Home() {
|
||||
</>
|
||||
}
|
||||
>
|
||||
{!ui.languagePromptDismissed && <LanguagePrompt />}
|
||||
<Heading>{t("Home")}</Heading>
|
||||
{showLanguagePrompt && <LanguagePrompt />}
|
||||
{teamOnboarding && (
|
||||
<>
|
||||
<Heading>{t("Welcome")},</Heading>
|
||||
<Onboarding>
|
||||
Check out the example documents we created below and then get
|
||||
started by creating a new collection of your own in the left
|
||||
sidebar…
|
||||
</Onboarding>
|
||||
</>
|
||||
)}
|
||||
{userOnboarding && (
|
||||
<>
|
||||
<Heading>{t("Welcome")},</Heading>
|
||||
<Onboarding>
|
||||
Outline is a place for you and your team to easily store and find
|
||||
knowledge. Start in the left sidebar to explore what other team
|
||||
members have created so far…
|
||||
</Onboarding>
|
||||
</>
|
||||
)}
|
||||
{!onboarding && (
|
||||
<>
|
||||
<Heading>{welcomeMessage},</Heading>
|
||||
<Onboarding>
|
||||
{recent.length ? (
|
||||
<Trans
|
||||
i18nKey="welcome"
|
||||
count={recent.length}
|
||||
defaults="{{ count }} docs were updated since you were last here"
|
||||
values={{ count: recent.length > 10 ? "10+" : recent.length }}
|
||||
/>
|
||||
) : (
|
||||
t("Here’s an overview of what’s been happening recently")
|
||||
)}
|
||||
…
|
||||
</Onboarding>
|
||||
</>
|
||||
)}
|
||||
<Tabs>
|
||||
<Tab to="/home" exact>
|
||||
{t("Recently updated")}
|
||||
@@ -64,9 +134,9 @@ function Home() {
|
||||
<Route path="/home/created">
|
||||
<PaginatedDocumentList
|
||||
key="created"
|
||||
documents={documents.createdByUser(user)}
|
||||
documents={documents.createdByUser(userId)}
|
||||
fetch={documents.fetchOwned}
|
||||
options={{ user }}
|
||||
options={{ userId }}
|
||||
showCollection
|
||||
/>
|
||||
</Route>
|
||||
@@ -82,4 +152,9 @@ function Home() {
|
||||
);
|
||||
}
|
||||
|
||||
const Onboarding = styled(HelpText)`
|
||||
margin-top: -12px;
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
export default observer(Home);
|
||||
|
||||
@@ -58,8 +58,12 @@ class UserListItem extends React.Component<Props> {
|
||||
) : (
|
||||
"Invited"
|
||||
)}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
|
||||
{user.isSuspended && <Badge>Suspended</Badge>}
|
||||
{user.isAdmin && (
|
||||
<Badge spaced primary={user.isAdmin}>
|
||||
Admin
|
||||
</Badge>
|
||||
)}
|
||||
{user.isSuspended && <Badge spaced>Suspended</Badge>}
|
||||
</>
|
||||
}
|
||||
actions={showMenu ? <UserMenu user={user} /> : undefined}
|
||||
|
||||
@@ -67,6 +67,12 @@ export default class DocumentsStore extends BaseStore<Document> {
|
||||
);
|
||||
}
|
||||
|
||||
updatedSinceTimestamp(since: string): Document[] {
|
||||
return this.recentlyUpdated.filter(
|
||||
(document) => document.updatedAt > since
|
||||
);
|
||||
}
|
||||
|
||||
createdByUser(userId: string): Document[] {
|
||||
return orderBy(
|
||||
filter(this.all, (d) => d.createdBy.id === userId),
|
||||
|
||||
@@ -100,7 +100,10 @@ router.get("email.callback", auth({ required: false }), async (ctx) => {
|
||||
await user.update({ lastActiveAt: new Date() });
|
||||
|
||||
// set cookies on response and redirect to team subdomain
|
||||
ctx.signIn(user, team, "email", false);
|
||||
ctx.signIn(user, team, "email", {
|
||||
isNewUser: false,
|
||||
isNewTeam: false,
|
||||
});
|
||||
} catch (err) {
|
||||
ctx.redirect(`${process.env.URL}?notice=expired-token`);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,12 @@ export default function auth(options?: { required?: boolean } = {}) {
|
||||
ctx.state.user = user;
|
||||
}
|
||||
|
||||
ctx.signIn = (user: User, team: Team, service, isFirstSignin = false) => {
|
||||
ctx.signIn = (
|
||||
user: User,
|
||||
team: Team,
|
||||
service,
|
||||
options: { isNewUser: boolean, isNewTeam: boolean }
|
||||
) => {
|
||||
if (user.isSuspended) {
|
||||
return ctx.redirect("/?notice=suspended");
|
||||
}
|
||||
@@ -157,7 +162,11 @@ export default function auth(options?: { required?: boolean } = {}) {
|
||||
httpOnly: false,
|
||||
expires,
|
||||
});
|
||||
ctx.redirect(`${team.url}/home${isFirstSignin ? "?welcome" : ""}`);
|
||||
ctx.redirect(
|
||||
`${team.url}/home${
|
||||
options.isNewTeam ? "?newTeam" : options.isNewUser ? "?newUser" : ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ export default function createMiddleware(providerName: string) {
|
||||
return ctx.redirect("/?notice=suspended");
|
||||
}
|
||||
|
||||
ctx.signIn(result.user, result.team, providerName, result.isNewUser);
|
||||
ctx.signIn(result.user, result.team, providerName, {
|
||||
isNewUser: result.isNewUser,
|
||||
isNewTeam: result.isNewTeam,
|
||||
});
|
||||
}
|
||||
)(ctx);
|
||||
};
|
||||
|
||||
+4
-1
@@ -17,6 +17,9 @@ export type ContextWithAuthMiddleware = {|
|
||||
user: User,
|
||||
team: Team,
|
||||
providerName: string,
|
||||
isFirstSignin: boolean
|
||||
options: {
|
||||
isNewUser: boolean,
|
||||
isNewTeam: boolean,
|
||||
}
|
||||
) => void,
|
||||
|};
|
||||
|
||||
@@ -294,6 +294,12 @@
|
||||
"Invite them to {{teamName}}": "Invite them to {{teamName}}",
|
||||
"{{userName}} was removed from the group": "{{userName}} was removed from the group",
|
||||
"This group has no members.": "This group has no members.",
|
||||
"Good evening": "Good evening",
|
||||
"Good afternoon": "Good afternoon",
|
||||
"Good morning": "Good morning",
|
||||
"welcome": "{{ count }} doc was updated since you were last here",
|
||||
"welcome_plural": "{{ count }} docs were updated since you were last here",
|
||||
"Nothing updated since you were last here": "Nothing updated since you were last here",
|
||||
"Recently viewed": "Recently viewed",
|
||||
"Created by me": "Created by me",
|
||||
"Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and there’s Markdown too.": "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and there’s Markdown too.",
|
||||
@@ -377,4 +383,4 @@
|
||||
"{{ time }} ago.": "{{ time }} ago.",
|
||||
"Edit Profile": "Edit Profile",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} hasn’t updated any documents yet."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user