feat: Allow users to override team setting for seamless editing (#5772)

This commit is contained in:
Tom Moor
2023-09-04 19:19:43 -04:00
committed by GitHub
parent c376dc1011
commit 74860ed961
10 changed files with 97 additions and 61 deletions

View File

@@ -40,7 +40,7 @@ import {
openDocumentComments,
} from "~/actions/definitions/documents";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
import useMobile from "~/hooks/useMobile";
import usePolicy from "~/hooks/usePolicy";
import useRequest from "~/hooks/useRequest";
@@ -73,7 +73,7 @@ function DocumentMenu({
onOpen,
onClose,
}: Props) {
const team = useCurrentTeam();
const user = useCurrentUser();
const { policies, collections, documents, subscriptions } = useStores();
const { showToast } = useToasts();
const menu = useMenuState({
@@ -263,7 +263,7 @@ function DocumentMenu({
type: "route",
title: t("Edit"),
to: documentEditPath(document),
visible: !!can.update && !team.seamlessEditing,
visible: !!can.update && user.separateEditMode,
icon: <EditIcon />,
},
{

View File

@@ -81,17 +81,6 @@ class Team extends BaseModel {
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.
*
* @returns True if editing mode is seamless (no button)
*/
@computed
get seamlessEditing(): boolean {
return !!this.getPreference(TeamPreference.SeamlessEdit);
}
/**
* Returns the value of the provided preference.
*
@@ -99,9 +88,15 @@ class Team extends BaseModel {
* @returns The preference value if set, else the default value
*/
getPreference<T extends keyof TeamPreferences>(
key: T
key: T,
defaultValue?: TeamPreferences[T]
): TeamPreferences[T] | false {
return this.preferences?.[key] ?? TeamPreferenceDefaults[key] ?? false;
return (
this.preferences?.[key] ??
TeamPreferenceDefaults[key] ??
defaultValue ??
false
);
}
/**

View File

@@ -5,6 +5,7 @@ import { UserPreferenceDefaults } from "@shared/constants";
import {
NotificationEventDefaults,
NotificationEventType,
TeamPreference,
UserPreference,
UserPreferences,
UserRole,
@@ -85,6 +86,20 @@ class User extends ParanoidModel {
}
}
/**
* Returns whether this user is using a separate editing mode behind an "Edit"
* button rather than seamless always-editing.
*
* @returns True if editing mode is seamless (no button)
*/
@computed
get separateEditMode(): boolean {
return !this.getPreference(
UserPreference.SeamlessEdit,
this.store.rootStore.auth.team.getPreference(TeamPreference.SeamlessEdit)
);
}
/**
* Returns the current preference for the given notification event type taking
* into account the default system value.
@@ -130,8 +145,10 @@ class User extends ParanoidModel {
* @param key The UserPreference key to retrieve
* @returns The value
*/
getPreference(key: UserPreference): boolean {
return this.preferences?.[key] ?? UserPreferenceDefaults[key] ?? false;
getPreference(key: UserPreference, defaultValue = false): boolean {
return (
this.preferences?.[key] ?? UserPreferenceDefaults[key] ?? defaultValue
);
}
/**

View File

@@ -72,7 +72,7 @@ function DataLoader({ match, children }: Props) {
? documents.getSharedTree(document.id)
: undefined;
const isEditRoute = match.path === matchDocumentEdit;
const isEditing = isEditRoute || !!auth.team?.seamlessEditing;
const isEditing = isEditRoute || !auth.user?.separateEditMode;
const can = usePolicy(document?.id);
const location = useLocation<LocationState>();

View File

@@ -391,7 +391,7 @@ class DocumentScene extends React.Component<Props> {
render() {
const { document, revision, readOnly, abilities, auth, ui, shareId, t } =
this.props;
const team = auth.team;
const { team, user } = auth;
const isShare = !!shareId;
const embedsDisabled =
(team && team.documentEmbeds === false) || document.embedsDisabled;
@@ -463,7 +463,7 @@ class DocumentScene extends React.Component<Props> {
revision={revision}
shareId={shareId}
isDraft={document.isDraft}
isEditing={!readOnly && !team?.seamlessEditing}
isEditing={!readOnly && !!user?.separateEditMode}
isSaving={this.isSaving}
isPublishing={this.isPublishing}
publishingIsDisabled={

View File

@@ -85,7 +85,7 @@ function DocumentHeader({
const { ui, auth } = useStores();
const theme = useTheme();
const { resolvedTheme } = ui;
const { team } = auth;
const { team, user } = auth;
const isMobile = useMobile();
const isRevision = !!revision;
@@ -224,11 +224,11 @@ function DocumentHeader({
<>
<ObservingBanner />
{!isPublishing && isSaving && !team?.seamlessEditing && (
{!isPublishing && isSaving && user?.separateEditMode && (
<Status>{t("Saving")}</Status>
)}
{!isDeleted && !isRevision && <Collaborators document={document} />}
{(isEditing || team?.seamlessEditing) && !isTemplate && isNew && (
{(isEditing || !user?.separateEditMode) && !isTemplate && isNew && (
<Action>
<TemplatesMenu
document={document}
@@ -267,7 +267,7 @@ function DocumentHeader({
)}
{can.update &&
!isEditing &&
!team?.seamlessEditing &&
user?.separateEditMode &&
!isRevision &&
editAction}
{can.update &&

View File

@@ -18,19 +18,19 @@ function Features() {
const { t } = useTranslation();
const { showToast } = useToasts();
const handlePreferenceChange = async (
ev: React.ChangeEvent<HTMLInputElement>
) => {
const preferences = {
...team.preferences,
[ev.target.name]: ev.target.checked,
};
const handlePreferenceChange =
(inverted = false) =>
async (ev: React.ChangeEvent<HTMLInputElement>) => {
const preferences = {
...team.preferences,
[ev.target.name]: inverted ? !ev.target.checked : ev.target.checked,
};
await auth.updateTeam({ preferences });
showToast(t("Settings saved"), {
type: "success",
});
};
await auth.updateTeam({ preferences });
showToast(t("Settings saved"), {
type: "success",
});
};
return (
<Scene title={t("Features")} icon={<BeakerIcon />}>
@@ -43,16 +43,16 @@ function Features() {
</Text>
<SettingRow
name={TeamPreference.SeamlessEdit}
label={t("Seamless editing")}
label={t("Separate editing")}
description={t(
`When enabled documents are always editable for team members that have permission. When disabled there is a separate editing view.`
`When enabled documents have a separate editing mode by default instead of being always editable. This setting can be overridden by user preferences.`
)}
>
<Switch
id={TeamPreference.SeamlessEdit}
name={TeamPreference.SeamlessEdit}
checked={team.getPreference(TeamPreference.SeamlessEdit)}
onChange={handlePreferenceChange}
checked={!team.getPreference(TeamPreference.SeamlessEdit)}
onChange={handlePreferenceChange(true)}
/>
</SettingRow>
<SettingRow
@@ -66,7 +66,7 @@ function Features() {
id={TeamPreference.Commenting}
name={TeamPreference.Commenting}
checked={team.getPreference(TeamPreference.Commenting)}
onChange={handlePreferenceChange}
onChange={handlePreferenceChange(false)}
/>
</SettingRow>
</Scene>

View File

@@ -3,13 +3,14 @@ import { SettingsIcon } from "outline-icons";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { languageOptions } from "@shared/i18n";
import { UserPreference } from "@shared/types";
import { TeamPreference, UserPreference } from "@shared/types";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import InputSelect from "~/components/InputSelect";
import Scene from "~/components/Scene";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
@@ -21,21 +22,22 @@ function Preferences() {
const { showToast } = useToasts();
const { dialogs, auth } = useStores();
const user = useCurrentUser();
const team = useCurrentTeam();
const handlePreferenceChange = async (
ev: React.ChangeEvent<HTMLInputElement>
) => {
const preferences = {
...user.preferences,
[ev.target.name]: ev.target.checked,
const handlePreferenceChange =
(inverted = false) =>
async (ev: React.ChangeEvent<HTMLInputElement>) => {
const preferences = {
...user.preferences,
[ev.target.name]: inverted ? !ev.target.checked : ev.target.checked,
};
await auth.updateUser({ preferences });
showToast(t("Preferences saved"), {
type: "success",
});
};
await auth.updateUser({ preferences });
showToast(t("Preferences saved"), {
type: "success",
});
};
const handleLanguageChange = async (language: string) => {
await auth.updateUser({ language });
showToast(t("Preferences saved"), {
@@ -98,7 +100,7 @@ function Preferences() {
id={UserPreference.UseCursorPointer}
name={UserPreference.UseCursorPointer}
checked={user.getPreference(UserPreference.UseCursorPointer)}
onChange={handlePreferenceChange}
onChange={handlePreferenceChange(false)}
/>
</SettingRow>
<SettingRow
@@ -111,11 +113,30 @@ function Preferences() {
id={UserPreference.CodeBlockLineNumers}
name={UserPreference.CodeBlockLineNumers}
checked={user.getPreference(UserPreference.CodeBlockLineNumers)}
onChange={handlePreferenceChange}
onChange={handlePreferenceChange(false)}
/>
</SettingRow>
<Heading as="h2">{t("Behavior")}</Heading>
<SettingRow
name={UserPreference.SeamlessEdit}
label={t("Separate editing")}
description={t(
`When enabled documents have a separate editing mode, when disabled documents are always editable when you have permission.`
)}
>
<Switch
id={UserPreference.SeamlessEdit}
name={UserPreference.SeamlessEdit}
checked={
!user.getPreference(
UserPreference.SeamlessEdit,
team.getPreference(TeamPreference.SeamlessEdit)
)
}
onChange={handlePreferenceChange(true)}
/>
</SettingRow>
<SettingRow
border={false}
name={UserPreference.RememberLastPath}
@@ -128,7 +149,7 @@ function Preferences() {
id={UserPreference.RememberLastPath}
name={UserPreference.RememberLastPath}
checked={!!user.getPreference(UserPreference.RememberLastPath)}
onChange={handlePreferenceChange}
onChange={handlePreferenceChange(false)}
/>
</SettingRow>

View File

@@ -781,8 +781,8 @@
"A full export might take some time, consider exporting a single document or collection. The exported data is a zip of your documents in Markdown format. You may leave this page once the export has started if you have notifications enabled, we will email a link to <em>{{ userEmail }}</em> when its complete.": "A full export might take some time, consider exporting a single document or collection. The exported data is a zip of your documents in Markdown format. You may leave this page once the export has started if you have notifications enabled, we will email a link to <em>{{ userEmail }}</em> when its complete.",
"Recent exports": "Recent exports",
"Manage optional and beta features. Changing these settings will affect the experience for all members of the workspace.": "Manage optional and beta features. Changing these settings will affect the experience for all members of the workspace.",
"Seamless editing": "Seamless editing",
"When enabled documents are always editable for team members that have permission. When disabled there is a separate editing view.": "When enabled documents are always editable for team members that have permission. When disabled there is a separate editing view.",
"Separate editing": "Separate editing",
"When enabled documents have a separate editing mode by default instead of being always editable. This setting can be overridden by user preferences.": "When enabled documents have a separate editing mode by default instead of being always editable. This setting can be overridden by user preferences.",
"Commenting": "Commenting",
"When enabled team members can add comments to documents.": "When enabled team members can add comments to documents.",
"Add a Google Analytics 4 measurement ID to send document views and analytics from the workspace to your own Google Analytics account.": "Add a Google Analytics 4 measurement ID to send document views and analytics from the workspace to your own Google Analytics account.",
@@ -834,6 +834,7 @@
"Show a hand cursor when hovering over interactive elements.": "Show a hand cursor when hovering over interactive elements.",
"Show line numbers": "Show line numbers",
"Show line numbers on code blocks in documents.": "Show line numbers on code blocks in documents.",
"When enabled documents have a separate editing mode, when disabled documents are always editable when you have permission.": "When enabled documents have a separate editing mode, when disabled documents are always editable when you have permission.",
"Remember previous location": "Remember previous location",
"Automatically return to the document you were last viewing when the app is re-opened.": "Automatically return to the document you were last viewing when the app is re-opened.",
"You may delete your account at any time, note that this is unrecoverable": "You may delete your account at any time, note that this is unrecoverable",

View File

@@ -114,6 +114,8 @@ export enum UserPreference {
UseCursorPointer = "useCursorPointer",
/** Whether code blocks should show line numbers. */
CodeBlockLineNumers = "codeBlockLineNumbers",
/** Whether documents have a separate edit mode instead of always editing. */
SeamlessEdit = "seamlessEdit",
}
export type UserPreferences = { [key in UserPreference]?: boolean };
@@ -130,7 +132,7 @@ export type PublicTeam = {
};
export enum TeamPreference {
/** Whether documents have a separate edit mode instead of seamless editing. */
/** Whether documents have a separate edit mode instead of always editing. */
SeamlessEdit = "seamlessEdit",
/** Whether to use team logo across the app for branding. */
PublicBranding = "publicBranding",