Compare commits

...

4 Commits

Author SHA1 Message Date
Tom Moor 0904b6a6a1 merge 2021-04-03 15:52:06 -07:00
Tom Moor 69995b5f9e Merge 2021-04-03 15:43:01 -07:00
Tom Moor e04c77dc25 wip 2021-04-03 15:14:07 -07:00
Tom Moor 41028eda4d wip 2021-02-20 15:27:35 -08:00
18 changed files with 250 additions and 67 deletions
+7 -3
View File
@@ -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>
+28
View File
@@ -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;
}
+6
View File
@@ -0,0 +1,6 @@
// @flow
import { useLocation } from "react-router-dom";
export default function useQuery() {
return new URLSearchParams(useLocation().search);
}
+18
View File
@@ -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
View File
@@ -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={
+1 -1
View File
@@ -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
View File
@@ -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("Heres an overview of whats 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}
+6
View File
@@ -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),
+4 -1
View File
@@ -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`);
}
+11 -2
View File
@@ -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" : ""
}`
);
}
};
+4 -1
View File
@@ -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
View File
@@ -17,6 +17,9 @@ export type ContextWithAuthMiddleware = {|
user: User,
team: Team,
providerName: string,
isFirstSignin: boolean
options: {
isNewUser: boolean,
isNewTeam: boolean,
}
) => void,
|};
+7 -1
View File
@@ -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 theres Markdown too.": "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and theres Markdown too.",
@@ -377,4 +383,4 @@
"{{ time }} ago.": "{{ time }} ago.",
"Edit Profile": "Edit Profile",
"{{ userName }} hasnt updated any documents yet.": "{{ userName }} hasnt updated any documents yet."
}
}