feat: Allow users to override team setting for seamless editing (#5772)
This commit is contained in:
@@ -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 />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -18,12 +18,12 @@ function Features() {
|
||||
const { t } = useTranslation();
|
||||
const { showToast } = useToasts();
|
||||
|
||||
const handlePreferenceChange = async (
|
||||
ev: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const handlePreferenceChange =
|
||||
(inverted = false) =>
|
||||
async (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const preferences = {
|
||||
...team.preferences,
|
||||
[ev.target.name]: ev.target.checked,
|
||||
[ev.target.name]: inverted ? !ev.target.checked : ev.target.checked,
|
||||
};
|
||||
|
||||
await auth.updateTeam({ preferences });
|
||||
@@ -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>
|
||||
|
||||
@@ -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,13 +22,14 @@ function Preferences() {
|
||||
const { showToast } = useToasts();
|
||||
const { dialogs, auth } = useStores();
|
||||
const user = useCurrentUser();
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const handlePreferenceChange = async (
|
||||
ev: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const handlePreferenceChange =
|
||||
(inverted = false) =>
|
||||
async (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const preferences = {
|
||||
...user.preferences,
|
||||
[ev.target.name]: ev.target.checked,
|
||||
[ev.target.name]: inverted ? !ev.target.checked : ev.target.checked,
|
||||
};
|
||||
|
||||
await auth.updateUser({ preferences });
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 it’s 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 it’s 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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user