From 74860ed961f3763c08e333c045a616f9a7aee985 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 4 Sep 2023 19:19:43 -0400 Subject: [PATCH] feat: Allow users to override team setting for seamless editing (#5772) --- app/menus/DocumentMenu.tsx | 6 +-- app/models/Team.ts | 21 +++----- app/models/User.ts | 21 +++++++- app/scenes/Document/components/DataLoader.tsx | 2 +- app/scenes/Document/components/Document.tsx | 4 +- app/scenes/Document/components/Header.tsx | 8 +-- app/scenes/Settings/Features.tsx | 34 ++++++------ app/scenes/Settings/Preferences.tsx | 53 +++++++++++++------ shared/i18n/locales/en_US/translation.json | 5 +- shared/types.ts | 4 +- 10 files changed, 97 insertions(+), 61 deletions(-) diff --git a/app/menus/DocumentMenu.tsx b/app/menus/DocumentMenu.tsx index bf1d62984..94806a293 100644 --- a/app/menus/DocumentMenu.tsx +++ b/app/menus/DocumentMenu.tsx @@ -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: , }, { diff --git a/app/models/Team.ts b/app/models/Team.ts index 1f8c09cbf..9130156da 100644 --- a/app/models/Team.ts +++ b/app/models/Team.ts @@ -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( - 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 + ); } /** diff --git a/app/models/User.ts b/app/models/User.ts index 8b7a38181..3b031d581 100644 --- a/app/models/User.ts +++ b/app/models/User.ts @@ -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 + ); } /** diff --git a/app/scenes/Document/components/DataLoader.tsx b/app/scenes/Document/components/DataLoader.tsx index 08b4bd63a..21b90e168 100644 --- a/app/scenes/Document/components/DataLoader.tsx +++ b/app/scenes/Document/components/DataLoader.tsx @@ -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(); diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx index eb996db8f..8e3542d40 100644 --- a/app/scenes/Document/components/Document.tsx +++ b/app/scenes/Document/components/Document.tsx @@ -391,7 +391,7 @@ class DocumentScene extends React.Component { 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 { revision={revision} shareId={shareId} isDraft={document.isDraft} - isEditing={!readOnly && !team?.seamlessEditing} + isEditing={!readOnly && !!user?.separateEditMode} isSaving={this.isSaving} isPublishing={this.isPublishing} publishingIsDisabled={ diff --git a/app/scenes/Document/components/Header.tsx b/app/scenes/Document/components/Header.tsx index 5107d0c15..3a47545de 100644 --- a/app/scenes/Document/components/Header.tsx +++ b/app/scenes/Document/components/Header.tsx @@ -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({ <> - {!isPublishing && isSaving && !team?.seamlessEditing && ( + {!isPublishing && isSaving && user?.separateEditMode && ( {t("Saving")}… )} {!isDeleted && !isRevision && } - {(isEditing || team?.seamlessEditing) && !isTemplate && isNew && ( + {(isEditing || !user?.separateEditMode) && !isTemplate && isNew && ( - ) => { - const preferences = { - ...team.preferences, - [ev.target.name]: ev.target.checked, - }; + const handlePreferenceChange = + (inverted = false) => + async (ev: React.ChangeEvent) => { + 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 ( }> @@ -43,16 +43,16 @@ function Features() { diff --git a/app/scenes/Settings/Preferences.tsx b/app/scenes/Settings/Preferences.tsx index ac56db4a9..62008ae30 100644 --- a/app/scenes/Settings/Preferences.tsx +++ b/app/scenes/Settings/Preferences.tsx @@ -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 - ) => { - const preferences = { - ...user.preferences, - [ev.target.name]: ev.target.checked, + const handlePreferenceChange = + (inverted = false) => + async (ev: React.ChangeEvent) => { + 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)} /> {t("Behavior")} + + + diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 7b14da6f2..e92c0f352 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -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 {{ userEmail }} 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 {{ userEmail }} 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", diff --git a/shared/types.ts b/shared/types.ts index d9a13bb2a..3802e52ab 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -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",