mirror of
https://github.com/outline/outline.git
synced 2026-08-03 13:27:25 +03:00
chore: Audit and move inline actions to definitions (#13133)
* chore: Audit and move inline actions to definitions * fix: Reuse existing smart-quote translation key in emoji delete dialog Co-Authored-By: Claude <noreply@anthropic.com> * Remove two usages of 'Link copied' translation --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,28 +7,23 @@ import type ApiKey from "~/models/ApiKey";
|
||||
import ApiKeyNew from "~/scenes/ApiKeyNew";
|
||||
import ApiKeyRevokeDialog from "~/scenes/Settings/components/ApiKeyRevokeDialog";
|
||||
import { createAction } from "..";
|
||||
import { dialogActionFactory } from "./common";
|
||||
import { SettingsSection } from "../sections";
|
||||
|
||||
export const createApiKey = createAction({
|
||||
name: ({ t }) => t("New API key"),
|
||||
export const createApiKey = dialogActionFactory({
|
||||
analyticsName: "New API key",
|
||||
section: SettingsSection,
|
||||
name: (t) => t("New API key"),
|
||||
title: (t) => t("New API key"),
|
||||
content: (onSubmit) => <ApiKeyNew onSubmit={onSubmit} />,
|
||||
icon: <PlusIcon />,
|
||||
keywords: "create",
|
||||
stopEvent: true,
|
||||
visible: () =>
|
||||
stores.policies.abilities(stores.auth.team?.id || "").createApiKey,
|
||||
perform: ({ t, event }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("New API key"),
|
||||
content: <ApiKeyNew onSubmit={stores.dialogs.closeAllModals} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const copyApiKeyFactory = ({ apiKey }: { apiKey: ApiKey }) =>
|
||||
export const copyApiKeyActionFactory = ({ apiKey }: { apiKey: ApiKey }) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Copy"),
|
||||
analyticsName: "Copy API key",
|
||||
@@ -44,7 +39,7 @@ export const copyApiKeyFactory = ({ apiKey }: { apiKey: ApiKey }) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const revokeApiKeyFactory = ({ apiKey }: { apiKey: ApiKey }) =>
|
||||
export const revokeApiKeyActionFactory = ({ apiKey }: { apiKey: ApiKey }) =>
|
||||
createAction({
|
||||
name: ({ t, isMenu }) =>
|
||||
isMenu
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
createInternalLinkAction,
|
||||
createActionWithChildren,
|
||||
} from "~/actions";
|
||||
import { dialogActionFactory } from "~/actions/definitions/common";
|
||||
import { ActiveCollectionSection, CollectionSection } from "~/actions/sections";
|
||||
import { setPersistedState } from "~/hooks/usePersistedState";
|
||||
import {
|
||||
@@ -77,22 +78,17 @@ export const openCollection = createActionWithChildren({
|
||||
},
|
||||
});
|
||||
|
||||
export const createCollection = createAction({
|
||||
name: ({ t }) => t("New collection"),
|
||||
export const createCollection = dialogActionFactory({
|
||||
analyticsName: "New collection",
|
||||
section: CollectionSection,
|
||||
name: (t) => t("New collection"),
|
||||
title: (t) => t("Create a collection"),
|
||||
content: (onSubmit) => <CollectionNew onSubmit={onSubmit} />,
|
||||
icon: <PlusIcon />,
|
||||
keywords: "create",
|
||||
stopEvent: true,
|
||||
visible: ({ stores }) =>
|
||||
stores.policies.abilities(stores.auth.team?.id || "").createCollection,
|
||||
perform: ({ t, event, stores }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
stores.dialogs.openModal({
|
||||
title: t("Create a collection"),
|
||||
content: <CollectionNew onSubmit={stores.dialogs.closeAllModals} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const editCollection = createAction({
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
import { DoneIcon, SmileyIcon, TrashIcon } from "outline-icons";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CopyIcon, DoneIcon, SmileyIcon, TrashIcon } from "outline-icons";
|
||||
import { toast } from "sonner";
|
||||
import type Comment from "~/models/Comment";
|
||||
import CommentDeleteDialog from "~/components/CommentDeleteDialog";
|
||||
import ViewReactionsDialog from "~/components/Reactions/ViewReactionsDialog";
|
||||
import { createAction } from "..";
|
||||
import { dialogActionFactory } from "./common";
|
||||
import { ActiveDocumentSection } from "../sections";
|
||||
import { commentPath, urlify } from "~/utils/routeHelpers";
|
||||
|
||||
export const deleteCommentFactory = ({
|
||||
export const deleteCommentActionFactory = ({
|
||||
comment,
|
||||
onDelete,
|
||||
}: {
|
||||
comment: Comment;
|
||||
onDelete: () => void;
|
||||
}) =>
|
||||
createAction({
|
||||
name: ({ t }) => `${t("Delete")}…`,
|
||||
dialogActionFactory({
|
||||
analyticsName: "Delete comment",
|
||||
section: ActiveDocumentSection,
|
||||
name: (t) => `${t("Delete")}…`,
|
||||
title: (t) => t("Delete comment"),
|
||||
content: () => (
|
||||
<CommentDeleteDialog comment={comment} onSubmit={onDelete} />
|
||||
),
|
||||
icon: <TrashIcon />,
|
||||
keywords: "trash",
|
||||
dangerous: true,
|
||||
stopEvent: true,
|
||||
visible: ({ stores }) => stores.policies.abilities(comment.id).delete,
|
||||
perform: ({ t, stores, event }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Delete comment"),
|
||||
content: <CommentDeleteDialog comment={comment} onSubmit={onDelete} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const resolveCommentFactory = ({
|
||||
export const resolveCommentActionFactory = ({
|
||||
comment,
|
||||
onResolve,
|
||||
}: {
|
||||
@@ -54,7 +53,7 @@ export const resolveCommentFactory = ({
|
||||
},
|
||||
});
|
||||
|
||||
export const unresolveCommentFactory = ({
|
||||
export const unresolveCommentActionFactory = ({
|
||||
comment,
|
||||
onUnresolve,
|
||||
}: {
|
||||
@@ -75,26 +74,42 @@ export const unresolveCommentFactory = ({
|
||||
},
|
||||
});
|
||||
|
||||
export const viewCommentReactionsFactory = ({
|
||||
export const copyCommentLinkActionFactory = ({
|
||||
comment,
|
||||
}: {
|
||||
comment: Comment;
|
||||
}) =>
|
||||
createAction({
|
||||
name: ({ t }) => `${t("View reactions")}`,
|
||||
name: ({ t }) => t("Copy link"),
|
||||
analyticsName: "Copy comment link",
|
||||
section: ActiveDocumentSection,
|
||||
icon: <CopyIcon />,
|
||||
keywords: "clipboard",
|
||||
perform: ({ stores, t }) => {
|
||||
const document = stores.documents.get(comment.documentId);
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
|
||||
copy(urlify(commentPath(document, comment)));
|
||||
toast.message(t("Link copied to clipboard"));
|
||||
},
|
||||
});
|
||||
|
||||
export const viewCommentReactionsActionFactory = ({
|
||||
comment,
|
||||
}: {
|
||||
comment: Comment;
|
||||
}) =>
|
||||
dialogActionFactory({
|
||||
analyticsName: "View comment reactions",
|
||||
section: ActiveDocumentSection,
|
||||
name: (t) => t("View reactions"),
|
||||
title: (t) => t("Reactions"),
|
||||
content: () => <ViewReactionsDialog model={comment} />,
|
||||
icon: <SmileyIcon />,
|
||||
stopEvent: true,
|
||||
visible: ({ stores }) =>
|
||||
stores.policies.abilities(comment.id).read &&
|
||||
comment.reactions.length > 0,
|
||||
perform: ({ t, stores, event }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Reactions"),
|
||||
content: <ViewReactionsDialog model={comment} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { TFunction } from "i18next";
|
||||
import { InputIcon } from "outline-icons";
|
||||
import stores from "~/stores";
|
||||
import type { Action } from "~/types";
|
||||
import { createAction } from "..";
|
||||
|
||||
/**
|
||||
* Creates an action that opens a dialog, taking care of wiring the dialog's
|
||||
* submit handler to close it again.
|
||||
*
|
||||
* @param analyticsName - untranslated name for analytics.
|
||||
* @param section - the section the action belongs to.
|
||||
* @param title - the dialog title.
|
||||
* @param content - renders the dialog, given a handler to close it.
|
||||
* @param name - the menu item label, defaults to the title with an ellipsis.
|
||||
* @param icon - optional icon for the menu item.
|
||||
* @param keywords - optional additional search terms for the command bar.
|
||||
* @param visible - optional visibility predicate.
|
||||
* @param dangerous - whether the action is destructive.
|
||||
* @param width - optional dialog width.
|
||||
* @param stopEvent - whether to suppress the triggering event before opening.
|
||||
* @returns an action for use in menus.
|
||||
*/
|
||||
export const dialogActionFactory = ({
|
||||
analyticsName,
|
||||
section,
|
||||
title,
|
||||
content,
|
||||
name,
|
||||
icon,
|
||||
keywords,
|
||||
visible,
|
||||
dangerous,
|
||||
width,
|
||||
stopEvent,
|
||||
}: {
|
||||
analyticsName: string;
|
||||
section: Action["section"];
|
||||
title: (t: TFunction) => string;
|
||||
content: (onSubmit: () => void) => React.ReactNode;
|
||||
name?: (t: TFunction) => string;
|
||||
icon?: React.ReactNode;
|
||||
keywords?: string;
|
||||
visible?: Action["visible"];
|
||||
dangerous?: boolean;
|
||||
width?: string | number;
|
||||
stopEvent?: boolean;
|
||||
}) =>
|
||||
createAction({
|
||||
name: ({ t }) => (name ? name(t) : `${title(t)}…`),
|
||||
analyticsName,
|
||||
section,
|
||||
icon,
|
||||
keywords,
|
||||
visible,
|
||||
dangerous,
|
||||
perform: ({ t, event }) => {
|
||||
if (stopEvent) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
}
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: title(t),
|
||||
content: content(stores.dialogs.closeAllModals),
|
||||
width,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates an action that begins renaming a model, either by switching an inline
|
||||
* title into edit mode or by opening a rename dialog.
|
||||
*
|
||||
* @param section - the section the action belongs to.
|
||||
* @param modelId - optional model to check the update ability against.
|
||||
* @param onRename - invoked when the action is performed.
|
||||
* @returns an action for use in menus.
|
||||
*/
|
||||
export const renameActionFactory = ({
|
||||
section,
|
||||
modelId,
|
||||
onRename,
|
||||
}: {
|
||||
section: Action["section"];
|
||||
modelId?: string;
|
||||
onRename?: () => void;
|
||||
}) =>
|
||||
createAction({
|
||||
name: ({ t }) => `${t("Rename")}…`,
|
||||
analyticsName: "Rename",
|
||||
section,
|
||||
icon: <InputIcon />,
|
||||
visible: ({ stores: rootStore }) =>
|
||||
!!onRename &&
|
||||
(modelId ? rootStore.policies.abilities(modelId).update : true),
|
||||
// Deferred a frame so the menu has closed before focus moves to the input.
|
||||
perform: () => requestAnimationFrame(() => onRename?.()),
|
||||
});
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
UserIcon,
|
||||
} from "outline-icons";
|
||||
import { toast } from "sonner";
|
||||
import { createAction, createActionWithChildren } from "~/actions";
|
||||
import {
|
||||
createAction,
|
||||
createActionWithChildren,
|
||||
createInternalLinkAction,
|
||||
} from "~/actions";
|
||||
import { DeveloperSection } from "~/actions/sections";
|
||||
import env from "~/env";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
@@ -19,14 +23,12 @@ import { deleteAllDatabases } from "~/utils/developer";
|
||||
import history from "~/utils/history";
|
||||
import { homePath, debugPath } from "~/utils/routeHelpers";
|
||||
|
||||
export const goToDebug = createAction({
|
||||
export const goToDebug = createInternalLinkAction({
|
||||
name: "Go to debug screen",
|
||||
icon: <BeakerIcon />,
|
||||
section: DeveloperSection,
|
||||
visible: () => env.ENVIRONMENT === "development",
|
||||
perform: () => {
|
||||
history.push(debugPath());
|
||||
},
|
||||
to: debugPath(),
|
||||
});
|
||||
|
||||
export const copyId = createActionWithChildren({
|
||||
|
||||
@@ -1208,7 +1208,7 @@ export const openRandomDocument = createAction({
|
||||
},
|
||||
});
|
||||
|
||||
export const searchDocumentsForQuery = (query: string) =>
|
||||
export const searchDocumentsForQueryActionFactory = (query: string) =>
|
||||
createInternalLinkAction({
|
||||
id: "search",
|
||||
name: ({ t }) =>
|
||||
@@ -1608,7 +1608,7 @@ export const leaveDocument = createAction({
|
||||
},
|
||||
});
|
||||
|
||||
export const applyTemplateFactory = ({
|
||||
export const applyTemplateActionFactory = ({
|
||||
actions,
|
||||
}: {
|
||||
actions: (Action | ActionGroup | ActionSeparator)[];
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import { createAction } from "~/actions";
|
||||
import { TeamSection } from "../sections";
|
||||
import { PlusIcon, ReplaceIcon, TrashIcon } from "outline-icons";
|
||||
import { dialogActionFactory } from "~/actions/definitions/common";
|
||||
import { EmojiSecion, TeamSection } from "../sections";
|
||||
import stores from "~/stores";
|
||||
import type Emoji from "~/models/Emoji";
|
||||
import { EmojiCreateDialog } from "~/components/EmojiDialog/EmojiCreateDialog";
|
||||
import { EmojiDeleteDialog } from "~/components/EmojiDialog/EmojiDeleteDialog";
|
||||
import { EmojiReplaceDialog } from "~/components/EmojiDialog/EmojiReplaceDialog";
|
||||
|
||||
export const createEmoji = createAction({
|
||||
name: ({ t }) => `${t("New emoji")}…`,
|
||||
export const createEmoji = dialogActionFactory({
|
||||
analyticsName: "Create emoji",
|
||||
section: TeamSection,
|
||||
name: (t) => `${t("New emoji")}…`,
|
||||
title: (t) => t("Upload emoji"),
|
||||
content: (onSubmit) => <EmojiCreateDialog onSubmit={onSubmit} />,
|
||||
icon: <PlusIcon />,
|
||||
keywords: "emoji custom upload image",
|
||||
section: TeamSection,
|
||||
visible: () =>
|
||||
stores.policies.abilities(stores.auth.team?.id || "").createEmoji,
|
||||
perform: ({ t }) => {
|
||||
stores.dialogs.openModal({
|
||||
title: t("Upload emoji"),
|
||||
content: <EmojiCreateDialog onSubmit={stores.dialogs.closeAllModals} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const replaceEmojiActionFactory = (emoji: Emoji) =>
|
||||
dialogActionFactory({
|
||||
analyticsName: "Replace emoji",
|
||||
section: EmojiSecion,
|
||||
name: (t) => `${t("Replace")}…`,
|
||||
title: (t) => t("Replace image"),
|
||||
content: (onSubmit) => (
|
||||
<EmojiReplaceDialog emoji={emoji} onSubmit={onSubmit} />
|
||||
),
|
||||
icon: <ReplaceIcon />,
|
||||
visible: () => stores.policies.abilities(emoji.id).update,
|
||||
});
|
||||
|
||||
export const deleteEmojiActionFactory = (emoji: Emoji) =>
|
||||
dialogActionFactory({
|
||||
analyticsName: "Delete emoji",
|
||||
section: EmojiSecion,
|
||||
name: (t) => `${t("Delete")}…`,
|
||||
title: (t) => t("Delete Emoji"),
|
||||
content: (onSubmit) => (
|
||||
<EmojiDeleteDialog emoji={emoji} onSubmit={onSubmit} />
|
||||
),
|
||||
icon: <TrashIcon />,
|
||||
dangerous: true,
|
||||
visible: () => stores.policies.abilities(emoji.id).delete,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { EditIcon, GroupIcon, TrashIcon } from "outline-icons";
|
||||
import stores from "~/stores";
|
||||
import type Group from "~/models/Group";
|
||||
import {
|
||||
DeleteGroupDialog,
|
||||
EditGroupDialog,
|
||||
} from "~/scenes/Settings/components/GroupDialogs";
|
||||
import { createInternalLinkAction } from "~/actions";
|
||||
import { dialogActionFactory } from "~/actions/definitions/common";
|
||||
import { GroupSection } from "~/actions/sections";
|
||||
import { settingsPath } from "~/utils/routeHelpers";
|
||||
|
||||
export const groupMembersActionFactory = (group: Group) =>
|
||||
createInternalLinkAction({
|
||||
name: ({ t }) => t("Members"),
|
||||
analyticsName: "Group members",
|
||||
section: GroupSection,
|
||||
icon: <GroupIcon />,
|
||||
visible: () => stores.policies.abilities(group.id).read,
|
||||
to: settingsPath("groups", group.id, "members"),
|
||||
});
|
||||
|
||||
export const editGroupActionFactory = (group: Group) =>
|
||||
dialogActionFactory({
|
||||
analyticsName: "Edit group",
|
||||
section: GroupSection,
|
||||
name: (t) => `${t("Edit")}…`,
|
||||
title: (t) => t("Edit group"),
|
||||
content: (onSubmit) => (
|
||||
<EditGroupDialog group={group} onSubmit={onSubmit} />
|
||||
),
|
||||
icon: <EditIcon />,
|
||||
visible: () => stores.policies.abilities(group.id).update,
|
||||
});
|
||||
|
||||
export const deleteGroupActionFactory = (group: Group) =>
|
||||
dialogActionFactory({
|
||||
analyticsName: "Delete group",
|
||||
section: GroupSection,
|
||||
name: (t) => `${t("Delete")}…`,
|
||||
title: (t) => t("Delete group"),
|
||||
content: (onSubmit) => (
|
||||
<DeleteGroupDialog group={group} onSubmit={onSubmit} />
|
||||
),
|
||||
icon: <TrashIcon />,
|
||||
dangerous: true,
|
||||
visible: () => stores.policies.abilities(group.id).delete,
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TrashIcon } from "outline-icons";
|
||||
import stores from "~/stores";
|
||||
import { createAction } from "..";
|
||||
import { dialogActionFactory } from "./common";
|
||||
import { SettingsSection } from "../sections";
|
||||
import type Integration from "~/models/Integration";
|
||||
import { DisconnectAnalyticsDialog } from "~/scenes/Settings/components/DisconnectAnalyticsDialog";
|
||||
@@ -8,7 +8,7 @@ import type { IntegrationType } from "@shared/types";
|
||||
import { settingsPath } from "@shared/utils/routeHelpers";
|
||||
import history from "~/utils/history";
|
||||
|
||||
export const disconnectIntegrationFactory = (integration?: Integration) =>
|
||||
export const disconnectIntegrationActionFactory = (integration?: Integration) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Disconnect"),
|
||||
analyticsName: "Disconnect integration",
|
||||
@@ -25,23 +25,20 @@ export const disconnectIntegrationFactory = (integration?: Integration) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const disconnectAnalyticsIntegrationFactory = (
|
||||
export const disconnectAnalyticsIntegrationActionFactory = (
|
||||
integration?: Integration<IntegrationType.Analytics>
|
||||
) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Disconnect analytics"),
|
||||
dialogActionFactory({
|
||||
analyticsName: "Disconnect analytics",
|
||||
section: SettingsSection,
|
||||
name: (t) => t("Disconnect analytics"),
|
||||
title: (t) => t("Disconnect analytics"),
|
||||
content: () =>
|
||||
integration ? (
|
||||
<DisconnectAnalyticsDialog integration={integration} />
|
||||
) : null,
|
||||
icon: <TrashIcon />,
|
||||
keywords: "disconnect",
|
||||
stopEvent: true,
|
||||
visible: () => !!integration,
|
||||
perform: ({ t, event }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Disconnect analytics"),
|
||||
content: <DisconnectAnalyticsDialog integration={integration!} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -46,7 +46,9 @@ export const navigateToHome = createInternalLinkAction({
|
||||
visible: ({ location }) => location.pathname !== homePath(),
|
||||
});
|
||||
|
||||
export const navigateToRecentSearchQuery = (searchQuery: SearchQuery) =>
|
||||
export const navigateToRecentSearchQueryActionFactory = (
|
||||
searchQuery: SearchQuery
|
||||
) =>
|
||||
createInternalLinkAction({
|
||||
section: RecentSearchesSection,
|
||||
name: searchQuery.query,
|
||||
|
||||
@@ -23,7 +23,7 @@ export const markNotificationsAsArchived = createAction({
|
||||
visible: ({ stores }) => stores.notifications.orderedData.length > 0,
|
||||
});
|
||||
|
||||
export const notificationMarkRead = (notification: Notification) =>
|
||||
export const notificationMarkReadActionFactory = (notification: Notification) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Mark as read"),
|
||||
analyticsName: "Mark notification read",
|
||||
@@ -33,7 +33,9 @@ export const notificationMarkRead = (notification: Notification) =>
|
||||
visible: () => !notification.viewedAt,
|
||||
});
|
||||
|
||||
export const notificationMarkUnread = (notification: Notification) =>
|
||||
export const notificationMarkUnreadActionFactory = (
|
||||
notification: Notification
|
||||
) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Mark as unread"),
|
||||
analyticsName: "Mark notification unread",
|
||||
@@ -43,7 +45,7 @@ export const notificationMarkUnread = (notification: Notification) =>
|
||||
visible: () => !!notification.viewedAt,
|
||||
});
|
||||
|
||||
export const notificationArchive = (notification: Notification) =>
|
||||
export const notificationArchiveActionFactory = (notification: Notification) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Archive"),
|
||||
analyticsName: "Mark notification as archived",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type OAuthAuthentication from "~/models/oauth/OAuthAuthentication";
|
||||
import OAuthAuthenticationRevokeDialog from "~/scenes/Settings/components/OAuthAuthenticationRevokeDialog";
|
||||
import { dialogActionFactory } from "./common";
|
||||
import { SettingsSection } from "../sections";
|
||||
|
||||
export const revokeOAuthAuthenticationActionFactory = ({
|
||||
oauthAuthentication,
|
||||
}: {
|
||||
oauthAuthentication: OAuthAuthentication;
|
||||
}) =>
|
||||
dialogActionFactory({
|
||||
analyticsName: "Revoke App",
|
||||
section: SettingsSection,
|
||||
name: (t) => t("Revoke"),
|
||||
title: (t) =>
|
||||
t("Revoke {{ appName }}", {
|
||||
appName: oauthAuthentication.oauthClient.name,
|
||||
}),
|
||||
content: (onSubmit) => (
|
||||
<OAuthAuthenticationRevokeDialog
|
||||
oauthAuthentication={oauthAuthentication}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
),
|
||||
dangerous: true,
|
||||
});
|
||||
@@ -1,24 +1,53 @@
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import stores from "~/stores";
|
||||
import type OAuthClient from "~/models/oauth/OAuthClient";
|
||||
import { OAuthClientNew } from "~/components/OAuthClient/OAuthClientNew";
|
||||
import { createAction } from "..";
|
||||
import OAuthClientDeleteDialog from "~/scenes/Settings/components/OAuthClientDeleteDialog";
|
||||
import { createInternalLinkAction } from "..";
|
||||
import { dialogActionFactory } from "./common";
|
||||
import { SettingsSection } from "../sections";
|
||||
import { settingsPath } from "~/utils/routeHelpers";
|
||||
|
||||
export const createOAuthClient = createAction({
|
||||
name: ({ t }) => t("New App"),
|
||||
export const createOAuthClient = dialogActionFactory({
|
||||
analyticsName: "New App",
|
||||
section: SettingsSection,
|
||||
name: (t) => t("New App"),
|
||||
title: (t) => t("New Application"),
|
||||
content: (onSubmit) => <OAuthClientNew onSubmit={onSubmit} />,
|
||||
icon: <PlusIcon />,
|
||||
keywords: "create",
|
||||
stopEvent: true,
|
||||
visible: () =>
|
||||
stores.policies.abilities(stores.auth.team?.id || "").createOAuthClient,
|
||||
perform: ({ t, event }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("New Application"),
|
||||
content: <OAuthClientNew onSubmit={stores.dialogs.closeAllModals} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const editOAuthClientActionFactory = ({
|
||||
oauthClient,
|
||||
visible,
|
||||
}: {
|
||||
oauthClient: OAuthClient;
|
||||
visible?: boolean;
|
||||
}) =>
|
||||
createInternalLinkAction({
|
||||
name: ({ t }) => `${t("Edit")}…`,
|
||||
analyticsName: "Edit App",
|
||||
section: SettingsSection,
|
||||
visible,
|
||||
to: settingsPath("applications", oauthClient.id),
|
||||
});
|
||||
|
||||
export const deleteOAuthClientActionFactory = ({
|
||||
oauthClient,
|
||||
}: {
|
||||
oauthClient: OAuthClient;
|
||||
}) =>
|
||||
dialogActionFactory({
|
||||
analyticsName: "Delete App",
|
||||
section: SettingsSection,
|
||||
name: (t) => `${t("Delete")}…`,
|
||||
title: (t) => t("Delete app"),
|
||||
content: (onSubmit) => (
|
||||
<OAuthClientDeleteDialog oauthClient={oauthClient} onSubmit={onSubmit} />
|
||||
),
|
||||
dangerous: true,
|
||||
});
|
||||
|
||||
@@ -5,7 +5,12 @@ import { toast } from "sonner";
|
||||
import { ExportContentType } from "@shared/types";
|
||||
import Revision from "~/models/Revision";
|
||||
import stores from "~/stores";
|
||||
import { createAction, createActionWithChildren } from "~/actions";
|
||||
import type { ActionContext } from "~/types";
|
||||
import {
|
||||
createAction,
|
||||
createActionWithChildren,
|
||||
createInternalLinkAction,
|
||||
} from "~/actions";
|
||||
import { RevisionSection } from "~/actions/sections";
|
||||
import env from "~/env";
|
||||
import history from "~/utils/history";
|
||||
@@ -15,36 +20,36 @@ import {
|
||||
urlify,
|
||||
} from "~/utils/routeHelpers";
|
||||
|
||||
export const restoreRevision = createAction({
|
||||
function getActiveRevisionId({ location, getActiveModel }: ActionContext) {
|
||||
const match = matchPath<{ revisionId: string }>(location.pathname, {
|
||||
path: matchDocumentHistory,
|
||||
});
|
||||
return getActiveModel(Revision)?.id ?? match?.params.revisionId;
|
||||
}
|
||||
|
||||
export const restoreRevision = createInternalLinkAction({
|
||||
name: ({ t }) => t("Restore"),
|
||||
analyticsName: "Restore revision",
|
||||
icon: <RestoreIcon />,
|
||||
section: RevisionSection,
|
||||
visible: ({ activeDocumentId }) =>
|
||||
!!activeDocumentId && stores.policies.abilities(activeDocumentId).update,
|
||||
perform: async ({ event, location, activeDocumentId, getActiveModel }) => {
|
||||
event?.preventDefault();
|
||||
if (!activeDocumentId) {
|
||||
return;
|
||||
visible: (context) =>
|
||||
!!context.activeDocumentId &&
|
||||
stores.policies.abilities(context.activeDocumentId).update &&
|
||||
!!getActiveRevisionId(context),
|
||||
to: (context) => {
|
||||
const revisionId = getActiveRevisionId(context);
|
||||
const document = context.activeDocumentId
|
||||
? stores.documents.get(context.activeDocumentId)
|
||||
: undefined;
|
||||
|
||||
if (!document || !revisionId) {
|
||||
return context.location;
|
||||
}
|
||||
|
||||
const match = matchPath<{ revisionId: string }>(location.pathname, {
|
||||
path: matchDocumentHistory,
|
||||
});
|
||||
const revisionId = getActiveModel(Revision)?.id ?? match?.params.revisionId;
|
||||
if (!revisionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.push(document.url, {
|
||||
restore: true,
|
||||
revisionId,
|
||||
});
|
||||
return {
|
||||
pathname: document.url,
|
||||
state: { restore: true, revisionId },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -80,7 +85,7 @@ export const deleteRevision = createAction({
|
||||
},
|
||||
});
|
||||
|
||||
export const copyLinkToRevision = (revisionId: string) =>
|
||||
export const copyLinkToRevisionActionFactory = (revisionId: string) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Copy link"),
|
||||
analyticsName: "Copy link to revision",
|
||||
@@ -101,13 +106,13 @@ export const copyLinkToRevision = (revisionId: string) =>
|
||||
copy(url, {
|
||||
format: "text/plain",
|
||||
onCopy: () => {
|
||||
toast.message(t("Link copied"));
|
||||
toast.message(t("Link copied to clipboard"));
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const downloadRevisionAsHTML = (revisionId: string) =>
|
||||
export const downloadRevisionAsHTMLActionFactory = (revisionId: string) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("HTML"),
|
||||
analyticsName: "Download revision as HTML",
|
||||
@@ -124,7 +129,7 @@ export const downloadRevisionAsHTML = (revisionId: string) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const downloadRevisionAsPDF = (revisionId: string) =>
|
||||
export const downloadRevisionAsPDFActionFactory = (revisionId: string) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("PDF"),
|
||||
analyticsName: "Download revision as PDF",
|
||||
@@ -147,7 +152,7 @@ export const downloadRevisionAsPDF = (revisionId: string) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const downloadRevisionAsMarkdown = (revisionId: string) =>
|
||||
export const downloadRevisionAsMarkdownActionFactory = (revisionId: string) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Markdown"),
|
||||
analyticsName: "Download revision as Markdown",
|
||||
@@ -164,7 +169,7 @@ export const downloadRevisionAsMarkdown = (revisionId: string) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const downloadRevision = (revisionId: string) =>
|
||||
export const downloadRevisionActionFactory = (revisionId: string) =>
|
||||
createActionWithChildren({
|
||||
name: ({ t, isMenu }) => (isMenu ? t("Download") : t("Download revision")),
|
||||
analyticsName: "Download revision",
|
||||
@@ -175,9 +180,9 @@ export const downloadRevision = (revisionId: string) =>
|
||||
!!activeDocumentId &&
|
||||
stores.policies.abilities(activeDocumentId).download,
|
||||
children: [
|
||||
downloadRevisionAsHTML(revisionId),
|
||||
downloadRevisionAsPDF(revisionId),
|
||||
downloadRevisionAsMarkdown(revisionId),
|
||||
downloadRevisionAsHTMLActionFactory(revisionId),
|
||||
downloadRevisionAsPDFActionFactory(revisionId),
|
||||
downloadRevisionAsMarkdownActionFactory(revisionId),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ShareSection } from "../sections";
|
||||
import env from "~/env";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const copyShareUrlFactory = ({ share }: { share: Share }) =>
|
||||
export const copyShareUrlActionFactory = ({ share }: { share: Share }) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Copy link"),
|
||||
analyticsName: "Copy share link",
|
||||
@@ -22,7 +22,7 @@ export const copyShareUrlFactory = ({ share }: { share: Share }) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const goToShareSourceFactory = ({ share }: { share: Share }) =>
|
||||
export const goToShareSourceActionFactory = ({ share }: { share: Share }) =>
|
||||
createInternalLinkAction({
|
||||
name: ({ t }) =>
|
||||
share.collectionId ? t("Go to collection") : t("Go to document"),
|
||||
@@ -35,7 +35,7 @@ export const goToShareSourceFactory = ({ share }: { share: Share }) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const revokeShareFactory = ({
|
||||
export const revokeShareActionFactory = ({
|
||||
share,
|
||||
can,
|
||||
}: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "~/actions";
|
||||
import type { ActionContext, ExternalLinkAction } from "~/types";
|
||||
import Desktop from "~/utils/Desktop";
|
||||
import { dialogActionFactory } from "./common";
|
||||
import { TeamSection } from "../sections";
|
||||
|
||||
export const switchTeamsList = ({ stores }: { stores: RootStore }) =>
|
||||
@@ -74,22 +75,16 @@ export const createTeam = createAction({
|
||||
},
|
||||
});
|
||||
|
||||
export const desktopLoginTeam = createAction({
|
||||
name: ({ t }) => t("Login to workspace"),
|
||||
export const desktopLoginTeam = dialogActionFactory({
|
||||
analyticsName: "Login to workspace",
|
||||
keywords: "change switch workspace organization team",
|
||||
section: TeamSection,
|
||||
name: (t) => t("Login to workspace"),
|
||||
title: (t) => t("Login to workspace"),
|
||||
content: () => <LoginDialog />,
|
||||
icon: <ArrowIcon />,
|
||||
keywords: "change switch workspace organization team",
|
||||
stopEvent: true,
|
||||
visible: () => Desktop.isElectron(),
|
||||
perform: ({ t, event, stores }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Login to workspace"),
|
||||
content: <LoginDialog />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const StyledTeamLogo = styled(TeamLogo)`
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CaseSensitiveIcon,
|
||||
CollectionIcon,
|
||||
CopyIcon,
|
||||
DuplicateIcon,
|
||||
MoveIcon,
|
||||
NewDocumentIcon,
|
||||
PlusIcon,
|
||||
@@ -177,6 +178,24 @@ export const createDocumentFromTemplate = createInternalLinkAction({
|
||||
},
|
||||
});
|
||||
|
||||
export const duplicateTemplate = createAction({
|
||||
name: ({ t }) => t("Duplicate"),
|
||||
analyticsName: "Duplicate template",
|
||||
section: ActiveTemplateSection,
|
||||
icon: <DuplicateIcon />,
|
||||
keywords: "copy",
|
||||
visible: ({ getActivePolicies }) =>
|
||||
getActivePolicies(Template).some((policy) => policy.abilities.duplicate),
|
||||
perform: async ({ getActiveModel, stores }) => {
|
||||
const template = getActiveModel(Template);
|
||||
if (!template) {
|
||||
return;
|
||||
}
|
||||
|
||||
await stores.templates.duplicate(template);
|
||||
},
|
||||
});
|
||||
|
||||
export const copyTemplateLink = createAction({
|
||||
name: ({ t }) => t("Copy link"),
|
||||
analyticsName: "Copy template link",
|
||||
|
||||
@@ -1,45 +1,52 @@
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import type { UserRole } from "@shared/types";
|
||||
import { toast } from "sonner";
|
||||
import { UserRole } from "@shared/types";
|
||||
import { errToString } from "@shared/utils/error";
|
||||
import { UserRoleHelper } from "@shared/utils/UserRoleHelper";
|
||||
import stores from "~/stores";
|
||||
import type User from "~/models/User";
|
||||
import Invite from "~/scenes/Invite";
|
||||
import {
|
||||
UserChangeAvatarDialog,
|
||||
UserChangeEmailDialog,
|
||||
UserChangeNameDialog,
|
||||
UserChangeRoleDialog,
|
||||
UserDeleteDialog,
|
||||
UserSuspendDialog,
|
||||
} from "~/components/UserDialogs";
|
||||
import { createAction } from "~/actions";
|
||||
import { createAction, createActionWithChildren } from "~/actions";
|
||||
import { dialogActionFactory } from "~/actions/definitions/common";
|
||||
import { UserSection } from "~/actions/sections";
|
||||
|
||||
export const inviteUser = createAction({
|
||||
name: ({ t }) => `${t("Invite people")}…`,
|
||||
export const inviteUser = dialogActionFactory({
|
||||
analyticsName: "Invite people",
|
||||
section: UserSection,
|
||||
name: (t) => `${t("Invite people")}…`,
|
||||
title: (t) => t("Invite to workspace"),
|
||||
content: (onSubmit) => <Invite onSubmit={onSubmit} />,
|
||||
icon: <PlusIcon />,
|
||||
keywords: "team member workspace user",
|
||||
section: UserSection,
|
||||
width: "500px",
|
||||
visible: () =>
|
||||
stores.policies.abilities(stores.auth.team?.id || "").inviteUser,
|
||||
perform: ({ t }) => {
|
||||
stores.dialogs.openModal({
|
||||
title: t("Invite to workspace"),
|
||||
width: "500px",
|
||||
content: <Invite onSubmit={stores.dialogs.closeAllModals} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const updateUserRoleActionFactory = (user: User, role: UserRole) =>
|
||||
createAction({
|
||||
name: ({ t }) =>
|
||||
UserRoleHelper.isRoleHigher(role, user!.role)
|
||||
dialogActionFactory({
|
||||
analyticsName: "Update user role",
|
||||
section: UserSection,
|
||||
name: (t) =>
|
||||
UserRoleHelper.isRoleHigher(role, user.role)
|
||||
? `${t("Promote to {{ role }}", {
|
||||
role: UserRoleHelper.displayName(role, t),
|
||||
})}…`
|
||||
: `${t("Demote to {{ role }}", {
|
||||
role: UserRoleHelper.displayName(role, t),
|
||||
})}…`,
|
||||
analyticsName: "Update user role",
|
||||
section: UserSection,
|
||||
title: (t) => t("Update role"),
|
||||
content: (onSubmit) => (
|
||||
<UserChangeRoleDialog user={user} role={role} onSubmit={onSubmit} />
|
||||
),
|
||||
visible: () => {
|
||||
const can = stores.policies.abilities(user.id);
|
||||
|
||||
@@ -49,18 +56,102 @@ export const updateUserRoleActionFactory = (user: User, role: UserRole) =>
|
||||
? can.demote
|
||||
: false;
|
||||
},
|
||||
perform: ({ t }) => {
|
||||
stores.dialogs.openModal({
|
||||
title: t("Update role"),
|
||||
content: (
|
||||
<UserChangeRoleDialog
|
||||
user={user}
|
||||
role={role}
|
||||
onSubmit={stores.dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
export const changeUserRoleActionFactory = (user: User) =>
|
||||
createActionWithChildren({
|
||||
name: ({ t }) => t("Change role"),
|
||||
analyticsName: "Change user role",
|
||||
section: UserSection,
|
||||
visible: () => {
|
||||
const can = stores.policies.abilities(user.id);
|
||||
return can.demote || can.promote;
|
||||
},
|
||||
children: [UserRole.Admin, UserRole.Member, UserRole.Viewer].map((role) =>
|
||||
updateUserRoleActionFactory(user, role)
|
||||
),
|
||||
});
|
||||
|
||||
export const changeUserAvatarActionFactory = (user: User) =>
|
||||
dialogActionFactory({
|
||||
analyticsName: "Change user avatar",
|
||||
section: UserSection,
|
||||
title: (t) => t("Change profile picture"),
|
||||
content: (onSubmit) => (
|
||||
<UserChangeAvatarDialog user={user} onSubmit={onSubmit} />
|
||||
),
|
||||
visible: () => stores.policies.abilities(user.id).update,
|
||||
});
|
||||
|
||||
export const changeUserNameActionFactory = (user: User) =>
|
||||
dialogActionFactory({
|
||||
analyticsName: "Change user name",
|
||||
section: UserSection,
|
||||
title: (t) => t("Change name"),
|
||||
content: (onSubmit) => (
|
||||
<UserChangeNameDialog user={user} onSubmit={onSubmit} />
|
||||
),
|
||||
visible: () => stores.policies.abilities(user.id).update,
|
||||
});
|
||||
|
||||
export const changeUserEmailActionFactory = (user: User) =>
|
||||
dialogActionFactory({
|
||||
analyticsName: "Change user email",
|
||||
section: UserSection,
|
||||
title: (t) => t("Change email"),
|
||||
content: (onSubmit) => (
|
||||
<UserChangeEmailDialog user={user} onSubmit={onSubmit} />
|
||||
),
|
||||
visible: () => stores.policies.abilities(user.id).update,
|
||||
});
|
||||
|
||||
export const suspendUserActionFactory = (user: User) =>
|
||||
dialogActionFactory({
|
||||
analyticsName: "Suspend user",
|
||||
section: UserSection,
|
||||
title: (t) => t("Suspend user"),
|
||||
content: (onSubmit) => (
|
||||
<UserSuspendDialog user={user} onSubmit={onSubmit} />
|
||||
),
|
||||
dangerous: true,
|
||||
visible: () => !user.isInvited && !user.isSuspended,
|
||||
});
|
||||
|
||||
export const resendInviteActionFactory = (user: User) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Resend invite"),
|
||||
analyticsName: "Resend invite",
|
||||
section: UserSection,
|
||||
visible: () => stores.policies.abilities(user.id).resendInvite,
|
||||
perform: async ({ t }) => {
|
||||
try {
|
||||
await stores.users.resendInvite(user);
|
||||
toast.success(
|
||||
t("Invite was resent to {{ userName }}", { userName: user.name })
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error(errToString(err));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const revokeInviteActionFactory = (user: User) =>
|
||||
createAction({
|
||||
name: ({ t }) => `${t("Revoke invite")}…`,
|
||||
analyticsName: "Revoke invite",
|
||||
section: UserSection,
|
||||
dangerous: true,
|
||||
visible: () => user.isInvited,
|
||||
perform: () => stores.users.delete(user),
|
||||
});
|
||||
|
||||
export const activateUserActionFactory = (user: User) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Activate user"),
|
||||
analyticsName: "Activate user",
|
||||
section: UserSection,
|
||||
visible: () => !user.isInvited && user.isSuspended,
|
||||
perform: () => stores.users.activate(user),
|
||||
});
|
||||
|
||||
export const deleteUserActionFactory = (userId: string) =>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
||||
import type Emoji from "~/models/Emoji";
|
||||
|
||||
interface Props {
|
||||
/** The emoji being deleted. */
|
||||
emoji: Emoji;
|
||||
/** Callback invoked after a successful deletion. */
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog confirming deletion of an existing custom emoji.
|
||||
*/
|
||||
export function EmojiDeleteDialog({ emoji, onSubmit }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await emoji.delete();
|
||||
onSubmit();
|
||||
toast.success(t("Emoji deleted"));
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
onSubmit={handleSubmit}
|
||||
submitText={t("I’m sure – Delete")}
|
||||
savingText={`${t("Deleting")}…`}
|
||||
danger
|
||||
>
|
||||
<Trans
|
||||
defaults="Are you sure you want to delete the <em>{{emojiName}}</em> emoji? You will no longer be able to use it in your documents or collections."
|
||||
values={{
|
||||
emojiName: emoji.name,
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
@@ -15,13 +15,12 @@ import Time from "../Time";
|
||||
import { UnreadBadge } from "../UnreadBadge";
|
||||
import lazyWithRetry from "~/utils/lazyWithRetry";
|
||||
import { ContextMenu } from "../Menu/ContextMenu";
|
||||
import { createActionWithChildren } from "~/actions";
|
||||
import {
|
||||
notificationMarkRead,
|
||||
notificationMarkUnread,
|
||||
notificationArchive,
|
||||
notificationMarkReadActionFactory,
|
||||
notificationMarkUnreadActionFactory,
|
||||
notificationArchiveActionFactory,
|
||||
} from "~/actions/definitions/notifications";
|
||||
import { NotificationSection } from "~/actions/sections";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
import AccessRequestActions from "./AccessRequestActions";
|
||||
|
||||
const CommentEditor = lazyWithRetry(
|
||||
@@ -56,19 +55,15 @@ function NotificationListItem({ notification, onNavigate }: Props) {
|
||||
onNavigate();
|
||||
};
|
||||
|
||||
const menuAction = React.useMemo(
|
||||
() =>
|
||||
createActionWithChildren({
|
||||
name: ({ t }) => t("Notification options"),
|
||||
section: NotificationSection,
|
||||
children: [
|
||||
notificationMarkRead(notification),
|
||||
notificationMarkUnread(notification),
|
||||
notificationArchive(notification),
|
||||
],
|
||||
}),
|
||||
const actions = React.useMemo(
|
||||
() => [
|
||||
notificationMarkReadActionFactory(notification),
|
||||
notificationMarkUnreadActionFactory(notification),
|
||||
notificationArchiveActionFactory(notification),
|
||||
],
|
||||
[notification]
|
||||
);
|
||||
const menuAction = useMenuAction(actions);
|
||||
|
||||
return (
|
||||
<ContextMenu action={menuAction} ariaLabel={t("Notification options")}>
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useKBar } from "kbar";
|
||||
import { observer } from "mobx-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Minute } from "@shared/utils/time";
|
||||
import { searchDocumentsForQuery } from "~/actions/definitions/documents";
|
||||
import { navigateToRecentSearchQuery } from "~/actions/definitions/navigation";
|
||||
import { searchDocumentsForQueryActionFactory } from "~/actions/definitions/documents";
|
||||
import { navigateToRecentSearchQueryActionFactory } from "~/actions/definitions/navigation";
|
||||
import useCommandBarActions from "~/hooks/useCommandBarActions";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
@@ -51,11 +51,13 @@ function SearchActions() {
|
||||
}, [documents, searchQuery]);
|
||||
|
||||
useCommandBarActions(
|
||||
searchQuery ? [searchDocumentsForQuery(searchQuery)] : [],
|
||||
searchQuery ? [searchDocumentsForQueryActionFactory(searchQuery)] : [],
|
||||
[searchQuery]
|
||||
);
|
||||
|
||||
useCommandBarActions(searches.recent.map(navigateToRecentSearchQuery));
|
||||
useCommandBarActions(
|
||||
searches.recent.map(navigateToRecentSearchQueryActionFactory)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
copyApiKeyFactory,
|
||||
revokeApiKeyFactory,
|
||||
copyApiKeyActionFactory,
|
||||
revokeApiKeyActionFactory,
|
||||
} from "~/actions/definitions/apiKeys";
|
||||
import type ApiKey from "~/models/ApiKey";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
@@ -14,7 +14,10 @@ import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
*/
|
||||
export function useApiKeyMenuActions(apiKey: ApiKey) {
|
||||
const actions = useMemo(
|
||||
() => [copyApiKeyFactory({ apiKey }), revokeApiKeyFactory({ apiKey })],
|
||||
() => [
|
||||
copyApiKeyActionFactory({ apiKey }),
|
||||
revokeApiKeyActionFactory({ apiKey }),
|
||||
],
|
||||
[apiKey]
|
||||
);
|
||||
return useMenuAction(actions);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { useMenuAction } from "./useMenuAction";
|
||||
import { ActionSeparator, createAction } from "~/actions";
|
||||
import { ActionSeparator } from "~/actions";
|
||||
import {
|
||||
deleteCollection,
|
||||
editCollection,
|
||||
@@ -19,11 +19,8 @@ import {
|
||||
openCollectionInSplit,
|
||||
sortCollection,
|
||||
} from "~/actions/definitions/collections";
|
||||
import { renameActionFactory } from "~/actions/definitions/common";
|
||||
import { ActiveCollectionSection } from "~/actions/sections";
|
||||
import { InputIcon } from "outline-icons";
|
||||
import usePolicy from "./usePolicy";
|
||||
import useStores from "./useStores";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
/** Collection ID for which the actions are generated */
|
||||
@@ -33,11 +30,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export function useCollectionMenuAction({ collectionId, onRename }: Props) {
|
||||
const { collections } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const collection = collections.get(collectionId);
|
||||
const can = usePolicy(collection);
|
||||
|
||||
const actions = useMemo(
|
||||
() => [
|
||||
restoreCollection,
|
||||
@@ -49,12 +41,10 @@ export function useCollectionMenuAction({ collectionId, onRename }: Props) {
|
||||
createDocument,
|
||||
importDocument,
|
||||
ActionSeparator,
|
||||
createAction({
|
||||
name: `${t("Rename")}…`,
|
||||
renameActionFactory({
|
||||
section: ActiveCollectionSection,
|
||||
icon: <InputIcon />,
|
||||
visible: !!can.update && !!onRename,
|
||||
perform: () => requestAnimationFrame(() => onRename?.()),
|
||||
modelId: collectionId,
|
||||
onRename,
|
||||
}),
|
||||
editCollection,
|
||||
editCollectionPermissions,
|
||||
@@ -67,7 +57,7 @@ export function useCollectionMenuAction({ collectionId, onRename }: Props) {
|
||||
ActionSeparator,
|
||||
deleteCollection,
|
||||
],
|
||||
[t, can.update, onRename]
|
||||
[collectionId, onRename]
|
||||
);
|
||||
|
||||
return useMenuAction(actions);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { InputIcon, SearchIcon } from "outline-icons";
|
||||
import { SearchIcon } from "outline-icons";
|
||||
import { ActionSeparator, createAction, createRootMenuAction } from "~/actions";
|
||||
import {
|
||||
restoreDocument,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
unpublishDocument,
|
||||
archiveDocument,
|
||||
moveDocument,
|
||||
applyTemplateFactory,
|
||||
applyTemplateActionFactory,
|
||||
pinDocument,
|
||||
openDocumentComments,
|
||||
openDocumentHistory,
|
||||
@@ -36,10 +36,10 @@ import {
|
||||
leaveDocument,
|
||||
permanentlyDeleteDocument,
|
||||
} from "~/actions/definitions/documents";
|
||||
import { renameActionFactory } from "~/actions/definitions/common";
|
||||
import { ActiveDocumentSection } from "~/actions/sections";
|
||||
import useMobile from "./useMobile";
|
||||
import type Template from "~/models/Template";
|
||||
import usePolicy from "./usePolicy";
|
||||
import { useTemplateMenuActions } from "./useTemplateMenuActions";
|
||||
|
||||
type Props = {
|
||||
@@ -61,7 +61,6 @@ export function useDocumentMenuAction({
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useMobile();
|
||||
const can = usePolicy(documentId);
|
||||
|
||||
const templateMenuActions = useTemplateMenuActions({
|
||||
documentId,
|
||||
@@ -86,12 +85,10 @@ export function useDocumentMenuAction({
|
||||
}),
|
||||
ActionSeparator,
|
||||
editDocument,
|
||||
createAction({
|
||||
name: `${t("Rename")}…`,
|
||||
renameActionFactory({
|
||||
section: ActiveDocumentSection,
|
||||
icon: <InputIcon />,
|
||||
visible: !!can.update && !!onRename,
|
||||
perform: () => requestAnimationFrame(() => onRename?.()),
|
||||
modelId: documentId,
|
||||
onRename,
|
||||
}),
|
||||
shareDocument,
|
||||
createTemplateFromDocument,
|
||||
@@ -100,7 +97,7 @@ export function useDocumentMenuAction({
|
||||
unpublishDocument,
|
||||
archiveDocument,
|
||||
moveDocument,
|
||||
applyTemplateFactory({ actions: templateMenuActions }),
|
||||
applyTemplateActionFactory({ actions: templateMenuActions }),
|
||||
importDocument,
|
||||
createNewDocument,
|
||||
createNewDocumentInAlphabeticalCollection,
|
||||
@@ -121,6 +118,6 @@ export function useDocumentMenuAction({
|
||||
permanentlyDeleteDocument,
|
||||
leaveDocument,
|
||||
]),
|
||||
[t, isMobile, templateMenuActions, can.update, onFindAndReplace, onRename]
|
||||
[t, isMobile, templateMenuActions, documentId, onFindAndReplace, onRename]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,135 +1,28 @@
|
||||
import * as React from "react";
|
||||
import { ReplaceIcon, TrashIcon } from "outline-icons";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { useMemo } from "react";
|
||||
import type Emoji from "~/models/Emoji";
|
||||
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
||||
import { EmojiReplaceDialog } from "~/components/EmojiDialog/EmojiReplaceDialog";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { createAction } from "~/actions";
|
||||
import { EmojiSecion } from "~/actions/sections";
|
||||
import {
|
||||
deleteEmojiActionFactory,
|
||||
replaceEmojiActionFactory,
|
||||
} from "~/actions/definitions/emojis";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
|
||||
/**
|
||||
* Hook that constructs the action menu for emoji management operations.
|
||||
*
|
||||
* @param targetEmoji - the emoji to build actions for, or null to skip.
|
||||
* @returns action with children for use in menus, or undefined if emoji is null.
|
||||
* @returns action with children for use in menus.
|
||||
*/
|
||||
export function useEmojiMenuActions(targetEmoji: Emoji | null) {
|
||||
const { t } = useTranslation();
|
||||
const { dialogs } = useStores();
|
||||
const can = usePolicy(targetEmoji ?? ({} as Emoji));
|
||||
|
||||
const openReplaceDialog = React.useCallback(() => {
|
||||
if (!targetEmoji) {
|
||||
return;
|
||||
}
|
||||
dialogs.openModal({
|
||||
title: t("Replace image"),
|
||||
content: (
|
||||
<EmojiReplaceDialog
|
||||
emoji={targetEmoji}
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [t, targetEmoji, dialogs]);
|
||||
|
||||
const openDeleteDialog = React.useCallback(() => {
|
||||
if (!targetEmoji) {
|
||||
return;
|
||||
}
|
||||
dialogs.openModal({
|
||||
title: t("Delete Emoji"),
|
||||
content: (
|
||||
<DeleteEmojiDialog
|
||||
emoji={targetEmoji}
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [t, targetEmoji, dialogs]);
|
||||
|
||||
const actionList = React.useMemo(() => {
|
||||
if (!targetEmoji) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const actions = [];
|
||||
|
||||
if (can.update) {
|
||||
actions.push(
|
||||
createAction({
|
||||
name: `${t("Replace")}…`,
|
||||
icon: <ReplaceIcon />,
|
||||
section: EmojiSecion,
|
||||
visible: true,
|
||||
perform: openReplaceDialog,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (can.delete) {
|
||||
actions.push(
|
||||
createAction({
|
||||
name: `${t("Delete")}…`,
|
||||
icon: <TrashIcon />,
|
||||
section: EmojiSecion,
|
||||
visible: true,
|
||||
dangerous: true,
|
||||
perform: openDeleteDialog,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}, [
|
||||
t,
|
||||
targetEmoji,
|
||||
can.update,
|
||||
can.delete,
|
||||
openReplaceDialog,
|
||||
openDeleteDialog,
|
||||
]);
|
||||
|
||||
return useMenuAction(actionList);
|
||||
}
|
||||
|
||||
const DeleteEmojiDialog = ({
|
||||
emoji,
|
||||
onSubmit,
|
||||
}: {
|
||||
emoji: Emoji;
|
||||
onSubmit: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (emoji) {
|
||||
await emoji.delete();
|
||||
onSubmit();
|
||||
toast.success(t("Emoji deleted"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
onSubmit={handleSubmit}
|
||||
submitText={t("I'm sure – Delete")}
|
||||
savingText={`${t("Deleting")}…`}
|
||||
danger
|
||||
>
|
||||
<Trans
|
||||
defaults="Are you sure you want to delete the <em>{{emojiName}}</em> emoji? You will no longer be able to use it in your documents or collections."
|
||||
values={{
|
||||
emojiName: emoji.name,
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</ConfirmationDialog>
|
||||
const actions = useMemo(
|
||||
() =>
|
||||
targetEmoji
|
||||
? [
|
||||
replaceEmojiActionFactory(targetEmoji),
|
||||
deleteEmojiActionFactory(targetEmoji),
|
||||
]
|
||||
: [],
|
||||
[targetEmoji]
|
||||
);
|
||||
};
|
||||
|
||||
return useMenuAction(actions);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import * as React from "react";
|
||||
import { EditIcon, GroupIcon, TrashIcon } from "outline-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useMemo } from "react";
|
||||
import type Group from "~/models/Group";
|
||||
import { ActionSeparator, createExternalLinkAction } from "~/actions";
|
||||
import {
|
||||
DeleteGroupDialog,
|
||||
EditGroupDialog,
|
||||
} from "~/scenes/Settings/components/GroupDialogs";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import {
|
||||
ActionSeparator,
|
||||
createAction,
|
||||
createExternalLinkAction,
|
||||
} from "~/actions";
|
||||
deleteGroupActionFactory,
|
||||
editGroupActionFactory,
|
||||
groupMembersActionFactory,
|
||||
} from "~/actions/definitions/groups";
|
||||
import { GroupSection } from "~/actions/sections";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
import { settingsPath } from "~/utils/routeHelpers";
|
||||
|
||||
interface Options {
|
||||
/** Whether to hide the "Members" navigation action. */
|
||||
@@ -28,87 +19,24 @@ interface Options {
|
||||
*
|
||||
* @param targetGroup - the group to build actions for, or null to skip.
|
||||
* @param options - optional configuration for the menu.
|
||||
* @returns action with children for use in menus, or undefined if group is null.
|
||||
* @returns action with children for use in menus.
|
||||
*/
|
||||
export function useGroupMenuActions(
|
||||
targetGroup: Group | null,
|
||||
options?: Options
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const { dialogs } = useStores();
|
||||
const history = useHistory();
|
||||
const can = usePolicy(targetGroup ?? ({} as Group));
|
||||
|
||||
const navigateToMembers = React.useCallback(() => {
|
||||
if (!targetGroup) {
|
||||
return;
|
||||
}
|
||||
history.push(settingsPath("groups", targetGroup.id, "members"));
|
||||
}, [targetGroup, history]);
|
||||
|
||||
const openEditDialog = React.useCallback(() => {
|
||||
if (!targetGroup) {
|
||||
return;
|
||||
}
|
||||
dialogs.openModal({
|
||||
title: t("Edit group"),
|
||||
content: (
|
||||
<EditGroupDialog
|
||||
group={targetGroup}
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [t, targetGroup, dialogs]);
|
||||
|
||||
const openDeleteDialog = React.useCallback(() => {
|
||||
if (!targetGroup) {
|
||||
return;
|
||||
}
|
||||
dialogs.openModal({
|
||||
title: t("Delete group"),
|
||||
content: (
|
||||
<DeleteGroupDialog
|
||||
group={targetGroup}
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [t, targetGroup, dialogs]);
|
||||
|
||||
const actionList = React.useMemo(
|
||||
const actions = useMemo(
|
||||
() =>
|
||||
!targetGroup
|
||||
? []
|
||||
: [
|
||||
...(options?.hideMembers
|
||||
? []
|
||||
: [
|
||||
createAction({
|
||||
name: t("Members"),
|
||||
icon: <GroupIcon />,
|
||||
section: GroupSection,
|
||||
visible: can.read,
|
||||
perform: navigateToMembers,
|
||||
}),
|
||||
ActionSeparator,
|
||||
]),
|
||||
createAction({
|
||||
name: `${t("Edit")}…`,
|
||||
icon: <EditIcon />,
|
||||
section: GroupSection,
|
||||
visible: can.update,
|
||||
perform: openEditDialog,
|
||||
}),
|
||||
createAction({
|
||||
name: `${t("Delete")}…`,
|
||||
icon: <TrashIcon />,
|
||||
section: GroupSection,
|
||||
visible: can.delete,
|
||||
dangerous: true,
|
||||
perform: openDeleteDialog,
|
||||
}),
|
||||
: [groupMembersActionFactory(targetGroup), ActionSeparator]),
|
||||
editGroupActionFactory(targetGroup),
|
||||
deleteGroupActionFactory(targetGroup),
|
||||
ActionSeparator,
|
||||
// Read-only rows surfacing the group's external identifiers.
|
||||
createExternalLinkAction({
|
||||
name: targetGroup.externalId ?? "",
|
||||
section: GroupSection,
|
||||
@@ -124,18 +52,8 @@ export function useGroupMenuActions(
|
||||
url: "",
|
||||
}),
|
||||
],
|
||||
[
|
||||
t,
|
||||
targetGroup,
|
||||
can.read,
|
||||
can.update,
|
||||
can.delete,
|
||||
options?.hideMembers,
|
||||
navigateToMembers,
|
||||
openEditDialog,
|
||||
openDeleteDialog,
|
||||
]
|
||||
[targetGroup, options?.hideMembers]
|
||||
);
|
||||
|
||||
return useMenuAction(actionList);
|
||||
return useMenuAction(actions);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import type Share from "~/models/Share";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import { ActionSeparator } from "~/actions";
|
||||
import {
|
||||
copyShareUrlFactory,
|
||||
goToShareSourceFactory,
|
||||
revokeShareFactory,
|
||||
copyShareUrlActionFactory,
|
||||
goToShareSourceActionFactory,
|
||||
revokeShareActionFactory,
|
||||
} from "~/actions/definitions/shares";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
|
||||
@@ -23,10 +23,10 @@ export function useShareMenuActions(targetShare: Share | null) {
|
||||
!targetShare
|
||||
? []
|
||||
: [
|
||||
copyShareUrlFactory({ share: targetShare }),
|
||||
goToShareSourceFactory({ share: targetShare }),
|
||||
copyShareUrlActionFactory({ share: targetShare }),
|
||||
goToShareSourceActionFactory({ share: targetShare }),
|
||||
ActionSeparator,
|
||||
revokeShareFactory({ share: targetShare, can }),
|
||||
revokeShareActionFactory({ share: targetShare, can }),
|
||||
],
|
||||
[targetShare, can]
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { DuplicateIcon, EditIcon } from "outline-icons";
|
||||
import { EditIcon } from "outline-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type Template from "~/models/Template";
|
||||
import { ActionSeparator, createAction } from "~/actions";
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
copyTemplate,
|
||||
createDocumentFromTemplate,
|
||||
deleteTemplate,
|
||||
duplicateTemplate,
|
||||
moveTemplate,
|
||||
} from "~/actions/definitions/templates";
|
||||
import { ActiveTemplateSection } from "~/actions/sections";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
|
||||
/**
|
||||
@@ -25,10 +26,8 @@ export function useTemplateSettingsActions(
|
||||
onEdit?: () => void
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const { templates } = useStores();
|
||||
const can = usePolicy(template ?? ({} as Template));
|
||||
const can = usePolicy(template);
|
||||
|
||||
const section = "Template";
|
||||
const actions = React.useMemo(
|
||||
() =>
|
||||
!template
|
||||
@@ -38,16 +37,10 @@ export function useTemplateSettingsActions(
|
||||
name: `${t("Edit")}…`,
|
||||
visible: !!can.update && !!onEdit,
|
||||
icon: <EditIcon />,
|
||||
section,
|
||||
section: ActiveTemplateSection,
|
||||
perform: () => onEdit?.(),
|
||||
}),
|
||||
createAction({
|
||||
name: t("Duplicate"),
|
||||
visible: !!can.duplicate,
|
||||
icon: <DuplicateIcon />,
|
||||
section,
|
||||
perform: () => templates.duplicate(template),
|
||||
}),
|
||||
duplicateTemplate,
|
||||
moveTemplate,
|
||||
ActionSeparator,
|
||||
createDocumentFromTemplate,
|
||||
@@ -55,7 +48,7 @@ export function useTemplateSettingsActions(
|
||||
ActionSeparator,
|
||||
deleteTemplate,
|
||||
],
|
||||
[can.update, can.duplicate, onEdit, t, template, templates]
|
||||
[can.update, onEdit, t, template]
|
||||
);
|
||||
|
||||
return useMenuAction(actions);
|
||||
|
||||
@@ -1,215 +1,45 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { UserRole } from "@shared/types";
|
||||
import { useMemo } from "react";
|
||||
import type User from "~/models/User";
|
||||
import { ActionSeparator } from "~/actions";
|
||||
import {
|
||||
ActionSeparator,
|
||||
createAction,
|
||||
createActionWithChildren,
|
||||
} from "~/actions";
|
||||
import {
|
||||
activateUserActionFactory,
|
||||
changeUserAvatarActionFactory,
|
||||
changeUserEmailActionFactory,
|
||||
changeUserNameActionFactory,
|
||||
changeUserRoleActionFactory,
|
||||
deleteUserActionFactory,
|
||||
updateUserRoleActionFactory,
|
||||
resendInviteActionFactory,
|
||||
revokeInviteActionFactory,
|
||||
suspendUserActionFactory,
|
||||
} from "~/actions/definitions/users";
|
||||
import { UserSection } from "~/actions/sections";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import {
|
||||
UserSuspendDialog,
|
||||
UserChangeNameDialog,
|
||||
UserChangeEmailDialog,
|
||||
UserChangeAvatarDialog,
|
||||
} from "~/components/UserDialogs";
|
||||
|
||||
/**
|
||||
* Hook that constructs the action menu for user management operations.
|
||||
*
|
||||
* @param targetUser - the user to build actions for, or null to skip.
|
||||
* @returns action with children for use in menus, or undefined if user is null.
|
||||
* @returns action with children for use in menus.
|
||||
*/
|
||||
export function useUserMenuActions(targetUser: User | null) {
|
||||
const { users, dialogs } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const can = usePolicy(targetUser ?? ({} as User));
|
||||
|
||||
const openAvatarDialog = React.useCallback(() => {
|
||||
if (!targetUser) {
|
||||
return;
|
||||
}
|
||||
dialogs.openModal({
|
||||
title: t("Change profile picture"),
|
||||
content: (
|
||||
<UserChangeAvatarDialog
|
||||
user={targetUser}
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [dialogs, t, targetUser]);
|
||||
|
||||
const openNameDialog = React.useCallback(() => {
|
||||
if (!targetUser) {
|
||||
return;
|
||||
}
|
||||
dialogs.openModal({
|
||||
title: t("Change name"),
|
||||
content: (
|
||||
<UserChangeNameDialog
|
||||
user={targetUser}
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [dialogs, t, targetUser]);
|
||||
|
||||
const openEmailDialog = React.useCallback(() => {
|
||||
if (!targetUser) {
|
||||
return;
|
||||
}
|
||||
dialogs.openModal({
|
||||
title: t("Change email"),
|
||||
content: (
|
||||
<UserChangeEmailDialog
|
||||
user={targetUser}
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [dialogs, t, targetUser]);
|
||||
|
||||
const openSuspendDialog = React.useCallback(() => {
|
||||
if (!targetUser) {
|
||||
return;
|
||||
}
|
||||
dialogs.openModal({
|
||||
title: t("Suspend user"),
|
||||
content: (
|
||||
<UserSuspendDialog
|
||||
user={targetUser}
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [dialogs, t, targetUser]);
|
||||
|
||||
const revokeInvitation = React.useCallback(async () => {
|
||||
if (!targetUser) {
|
||||
return;
|
||||
}
|
||||
await users.delete(targetUser);
|
||||
}, [users, targetUser]);
|
||||
|
||||
const resendInvitation = React.useCallback(async () => {
|
||||
if (!targetUser) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await users.resendInvite(targetUser);
|
||||
toast.success(t(`Invite was resent to ${targetUser.name}`));
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t(`An error occurred while sending the invite`)
|
||||
);
|
||||
}
|
||||
}, [users, targetUser, t]);
|
||||
|
||||
const activateUser = React.useCallback(async () => {
|
||||
if (!targetUser) {
|
||||
return;
|
||||
}
|
||||
await users.activate(targetUser);
|
||||
}, [users, targetUser]);
|
||||
|
||||
const roleChangeActions = React.useMemo(
|
||||
const actions = useMemo(
|
||||
() =>
|
||||
targetUser
|
||||
? [UserRole.Admin, UserRole.Member, UserRole.Viewer].map((role) =>
|
||||
updateUserRoleActionFactory(targetUser, role)
|
||||
)
|
||||
? [
|
||||
changeUserRoleActionFactory(targetUser),
|
||||
changeUserAvatarActionFactory(targetUser),
|
||||
changeUserNameActionFactory(targetUser),
|
||||
changeUserEmailActionFactory(targetUser),
|
||||
resendInviteActionFactory(targetUser),
|
||||
ActionSeparator,
|
||||
revokeInviteActionFactory(targetUser),
|
||||
activateUserActionFactory(targetUser),
|
||||
suspendUserActionFactory(targetUser),
|
||||
ActionSeparator,
|
||||
deleteUserActionFactory(targetUser.id),
|
||||
]
|
||||
: [],
|
||||
[targetUser]
|
||||
);
|
||||
|
||||
const actionList = React.useMemo(
|
||||
() =>
|
||||
!targetUser
|
||||
? []
|
||||
: [
|
||||
createActionWithChildren({
|
||||
name: t("Change role"),
|
||||
section: UserSection,
|
||||
visible: can.demote || can.promote,
|
||||
children: roleChangeActions,
|
||||
}),
|
||||
createAction({
|
||||
name: `${t("Change profile picture")}…`,
|
||||
section: UserSection,
|
||||
visible: can.update,
|
||||
perform: openAvatarDialog,
|
||||
}),
|
||||
createAction({
|
||||
name: `${t("Change name")}…`,
|
||||
section: UserSection,
|
||||
visible: can.update,
|
||||
perform: openNameDialog,
|
||||
}),
|
||||
createAction({
|
||||
name: `${t("Change email")}…`,
|
||||
section: UserSection,
|
||||
visible: can.update,
|
||||
perform: openEmailDialog,
|
||||
}),
|
||||
createAction({
|
||||
name: t("Resend invite"),
|
||||
section: UserSection,
|
||||
visible: can.resendInvite,
|
||||
perform: resendInvitation,
|
||||
}),
|
||||
ActionSeparator,
|
||||
createAction({
|
||||
name: `${t("Revoke invite")}…`,
|
||||
section: UserSection,
|
||||
visible: targetUser.isInvited,
|
||||
dangerous: true,
|
||||
perform: revokeInvitation,
|
||||
}),
|
||||
createAction({
|
||||
name: t("Activate user"),
|
||||
section: UserSection,
|
||||
visible: !targetUser.isInvited && targetUser.isSuspended,
|
||||
perform: activateUser,
|
||||
}),
|
||||
createAction({
|
||||
name: `${t("Suspend user")}…`,
|
||||
section: UserSection,
|
||||
visible: !targetUser.isInvited && !targetUser.isSuspended,
|
||||
dangerous: true,
|
||||
perform: openSuspendDialog,
|
||||
}),
|
||||
ActionSeparator,
|
||||
deleteUserActionFactory(targetUser.id),
|
||||
],
|
||||
[
|
||||
t,
|
||||
targetUser,
|
||||
can.demote,
|
||||
can.promote,
|
||||
can.update,
|
||||
can.resendInvite,
|
||||
roleChangeActions,
|
||||
openAvatarDialog,
|
||||
openNameDialog,
|
||||
openEmailDialog,
|
||||
resendInvitation,
|
||||
revokeInvitation,
|
||||
activateUser,
|
||||
openSuspendDialog,
|
||||
]
|
||||
);
|
||||
|
||||
return useMenuAction(actionList);
|
||||
return useMenuAction(actions);
|
||||
}
|
||||
|
||||
+13
-30
@@ -1,21 +1,18 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import { observer } from "mobx-react";
|
||||
import { CopyIcon, EditIcon } from "outline-icons";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { EditIcon } from "outline-icons";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import type Comment from "~/models/Comment";
|
||||
import { DropdownMenu } from "~/components/Menu/DropdownMenu";
|
||||
import { OverflowMenuButton } from "~/components/Menu/OverflowMenuButton";
|
||||
import {
|
||||
deleteCommentFactory,
|
||||
resolveCommentFactory,
|
||||
unresolveCommentFactory,
|
||||
viewCommentReactionsFactory,
|
||||
copyCommentLinkActionFactory,
|
||||
deleteCommentActionFactory,
|
||||
resolveCommentActionFactory,
|
||||
unresolveCommentActionFactory,
|
||||
viewCommentReactionsActionFactory,
|
||||
} from "~/actions/definitions/comments";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { commentPath, urlify } from "~/utils/routeHelpers";
|
||||
import { ActionSeparator, createAction } from "~/actions";
|
||||
import { ActiveDocumentSection } from "~/actions/sections";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
@@ -40,17 +37,8 @@ function CommentMenu({
|
||||
onUpdate,
|
||||
className,
|
||||
}: Props) {
|
||||
const { documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const can = usePolicy(comment);
|
||||
const document = documents.get(comment.documentId);
|
||||
|
||||
const handleCopyLink = useCallback(() => {
|
||||
if (document) {
|
||||
copy(urlify(commentPath(document, comment)));
|
||||
toast.message(t("Link copied"));
|
||||
}
|
||||
}, [t, document, comment]);
|
||||
|
||||
const actions = useMemo(
|
||||
() => [
|
||||
@@ -61,27 +49,22 @@ function CommentMenu({
|
||||
visible: can.update && !comment.isResolved,
|
||||
perform: onEdit,
|
||||
}),
|
||||
resolveCommentFactory({
|
||||
resolveCommentActionFactory({
|
||||
comment,
|
||||
onResolve: () => onUpdate({ resolved: true }),
|
||||
}),
|
||||
unresolveCommentFactory({
|
||||
unresolveCommentActionFactory({
|
||||
comment,
|
||||
onUnresolve: () => onUpdate({ resolved: false }),
|
||||
}),
|
||||
viewCommentReactionsFactory({
|
||||
viewCommentReactionsActionFactory({
|
||||
comment,
|
||||
}),
|
||||
createAction({
|
||||
name: t("Copy link"),
|
||||
icon: <CopyIcon />,
|
||||
section: ActiveDocumentSection,
|
||||
perform: handleCopyLink,
|
||||
}),
|
||||
copyCommentLinkActionFactory({ comment }),
|
||||
ActionSeparator,
|
||||
deleteCommentFactory({ comment, onDelete }),
|
||||
deleteCommentActionFactory({ comment, onDelete }),
|
||||
],
|
||||
[t, comment, can.update, onEdit, onUpdate, onDelete, handleCopyLink]
|
||||
[t, comment, can.update, onEdit, onUpdate, onDelete]
|
||||
);
|
||||
|
||||
const rootAction = useMenuAction(actions);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type OAuthAuthentication from "~/models/oauth/OAuthAuthentication";
|
||||
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
||||
import { DropdownMenu } from "~/components/Menu/DropdownMenu";
|
||||
import { OverflowMenuButton } from "~/components/Menu/OverflowMenuButton";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { createAction } from "~/actions";
|
||||
import { revokeOAuthAuthenticationActionFactory } from "~/actions/definitions/oauthAuthentications";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
|
||||
type Props = {
|
||||
@@ -15,40 +13,11 @@ type Props = {
|
||||
};
|
||||
|
||||
function OAuthAuthenticationMenu({ oauthAuthentication }: Props) {
|
||||
const { dialogs } = useStores();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleRevoke = useCallback(() => {
|
||||
dialogs.openModal({
|
||||
title: t("Revoke {{ appName }}", {
|
||||
appName: oauthAuthentication.oauthClient.name,
|
||||
}),
|
||||
content: (
|
||||
<ConfirmationDialog
|
||||
onSubmit={async () => {
|
||||
await oauthAuthentication.deleteAll();
|
||||
dialogs.closeAllModals();
|
||||
}}
|
||||
submitText={t("Revoke")}
|
||||
savingText={`${t("Revoking")}…`}
|
||||
danger
|
||||
>
|
||||
{t("Are you sure you want to revoke access?")}
|
||||
</ConfirmationDialog>
|
||||
),
|
||||
});
|
||||
}, [t, dialogs, oauthAuthentication]);
|
||||
|
||||
const actions = useMemo(
|
||||
() => [
|
||||
createAction({
|
||||
name: t("Revoke"),
|
||||
section: "OAuth",
|
||||
dangerous: true,
|
||||
perform: handleRevoke,
|
||||
}),
|
||||
],
|
||||
[t, handleRevoke]
|
||||
() => [revokeOAuthAuthenticationActionFactory({ oauthAuthentication })],
|
||||
[oauthAuthentication]
|
||||
);
|
||||
|
||||
const rootAction = useMenuAction(actions);
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type OAuthClient from "~/models/oauth/OAuthClient";
|
||||
import OAuthClientDeleteDialog from "~/scenes/Settings/components/OAuthClientDeleteDialog";
|
||||
import { DropdownMenu } from "~/components/Menu/DropdownMenu";
|
||||
import { OverflowMenuButton } from "~/components/Menu/OverflowMenuButton";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { settingsPath } from "~/utils/routeHelpers";
|
||||
import { ActionSeparator } from "~/actions";
|
||||
import {
|
||||
ActionSeparator,
|
||||
createAction,
|
||||
createInternalLinkAction,
|
||||
} from "~/actions";
|
||||
deleteOAuthClientActionFactory,
|
||||
editOAuthClientActionFactory,
|
||||
} from "~/actions/definitions/oauthClients";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
|
||||
const Section = "OAuth";
|
||||
|
||||
type Props = {
|
||||
/** The oauthClient to associate with the menu */
|
||||
oauthClient: OAuthClient;
|
||||
@@ -24,38 +19,15 @@ type Props = {
|
||||
};
|
||||
|
||||
function OAuthClientMenu({ oauthClient, showEdit }: Props) {
|
||||
const { dialogs } = useStores();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
dialogs.openModal({
|
||||
title: t("Delete app"),
|
||||
content: (
|
||||
<OAuthClientDeleteDialog
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
oauthClient={oauthClient}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [t, dialogs, oauthClient]);
|
||||
|
||||
const actions = useMemo(
|
||||
() => [
|
||||
createInternalLinkAction({
|
||||
name: `${t("Edit")}…`,
|
||||
section: Section,
|
||||
visible: showEdit,
|
||||
to: settingsPath("applications", oauthClient.id),
|
||||
}),
|
||||
editOAuthClientActionFactory({ oauthClient, visible: showEdit }),
|
||||
ActionSeparator,
|
||||
createAction({
|
||||
name: `${t("Delete")}…`,
|
||||
section: Section,
|
||||
dangerous: true,
|
||||
perform: handleDelete,
|
||||
}),
|
||||
deleteOAuthClientActionFactory({ oauthClient }),
|
||||
],
|
||||
[t, showEdit, oauthClient.id, handleDelete]
|
||||
[oauthClient, showEdit]
|
||||
);
|
||||
|
||||
const rootAction = useMenuAction(actions);
|
||||
|
||||
@@ -5,8 +5,8 @@ import { DropdownMenu } from "~/components/Menu/DropdownMenu";
|
||||
import { OverflowMenuButton } from "~/components/Menu/OverflowMenuButton";
|
||||
import { ActionSeparator } from "~/actions";
|
||||
import {
|
||||
copyLinkToRevision,
|
||||
downloadRevision,
|
||||
copyLinkToRevisionActionFactory,
|
||||
downloadRevisionActionFactory,
|
||||
restoreRevision,
|
||||
} from "~/actions/definitions/revisions";
|
||||
import { useMemo } from "react";
|
||||
@@ -24,8 +24,8 @@ function RevisionMenu({ document, revisionId }: Props) {
|
||||
() => [
|
||||
restoreRevision,
|
||||
ActionSeparator,
|
||||
copyLinkToRevision(revisionId),
|
||||
downloadRevision(revisionId),
|
||||
copyLinkToRevisionActionFactory(revisionId),
|
||||
downloadRevisionActionFactory(revisionId),
|
||||
],
|
||||
[revisionId]
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
|
||||
import Text from "~/components/Text";
|
||||
import Time from "~/components/Time";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
import { resolveCommentFactory } from "~/actions/definitions/comments";
|
||||
import { resolveCommentActionFactory } from "~/actions/definitions/comments";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import CommentMenu from "~/menus/CommentMenu";
|
||||
@@ -320,7 +320,7 @@ const ResolveButton = ({
|
||||
<Tooltip content={t("Mark as resolved")} placement="top">
|
||||
<Action
|
||||
as={NudeButton}
|
||||
action={resolveCommentFactory({
|
||||
action={resolveCommentActionFactory({
|
||||
comment,
|
||||
onResolve: () => onUpdate({ resolved: true }),
|
||||
})}
|
||||
|
||||
@@ -12,8 +12,8 @@ import type Document from "~/models/Document";
|
||||
import type Revision from "~/models/Revision";
|
||||
import { ActionSeparator } from "~/actions";
|
||||
import {
|
||||
copyLinkToRevision,
|
||||
downloadRevision,
|
||||
copyLinkToRevisionActionFactory,
|
||||
downloadRevisionActionFactory,
|
||||
restoreRevision,
|
||||
} from "~/actions/definitions/revisions";
|
||||
import { Avatar, AvatarSize } from "~/components/Avatar";
|
||||
@@ -50,8 +50,8 @@ const RevisionListItem = ({ item, document, ...rest }: Props) => {
|
||||
() => [
|
||||
restoreRevision,
|
||||
ActionSeparator,
|
||||
copyLinkToRevision(item.id),
|
||||
downloadRevision(item.id),
|
||||
copyLinkToRevisionActionFactory(item.id),
|
||||
downloadRevisionActionFactory(item.id),
|
||||
],
|
||||
[item.id]
|
||||
);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type OAuthAuthentication from "~/models/oauth/OAuthAuthentication";
|
||||
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
||||
|
||||
type Props = {
|
||||
oauthAuthentication: OAuthAuthentication;
|
||||
onSubmit: () => void;
|
||||
};
|
||||
|
||||
export default function OAuthAuthenticationRevokeDialog({
|
||||
oauthAuthentication,
|
||||
onSubmit,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await oauthAuthentication.deleteAll();
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
onSubmit={handleSubmit}
|
||||
submitText={t("Revoke")}
|
||||
savingText={`${t("Revoking")}…`}
|
||||
danger
|
||||
>
|
||||
{t("Are you sure you want to revoke access?")}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import type OAuthClient from "~/models/oauth/OAuthClient";
|
||||
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
||||
import { settingsPath } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
oauthClient: OAuthClient;
|
||||
@@ -12,8 +14,15 @@ export default function OAuthClientDeleteDialog({
|
||||
onSubmit,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Navigate back to the list if we're viewing the app being deleted.
|
||||
if (location.pathname === settingsPath("applications", oauthClient.id)) {
|
||||
history.push(settingsPath("applications"));
|
||||
}
|
||||
|
||||
await oauthClient.delete();
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import useStores from "~/hooks/useStores";
|
||||
import Icon from "./Icon";
|
||||
import Flex from "~/components/Flex";
|
||||
import styled from "styled-components";
|
||||
import { disconnectIntegrationFactory } from "~/actions/definitions/integrations";
|
||||
import { disconnectIntegrationActionFactory } from "~/actions/definitions/integrations";
|
||||
|
||||
type FormData = {
|
||||
url: string;
|
||||
@@ -110,7 +110,7 @@ function DiagramsNet() {
|
||||
</StyledSubmit>
|
||||
|
||||
<Button
|
||||
action={disconnectIntegrationFactory(integration)}
|
||||
action={disconnectIntegrationActionFactory(integration)}
|
||||
disabled={formState.isSubmitting}
|
||||
neutral
|
||||
hideIcon
|
||||
|
||||
@@ -15,7 +15,7 @@ import GoogleIcon from "~/components/Icons/GoogleIcon";
|
||||
import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { disconnectAnalyticsIntegrationFactory } from "~/actions/definitions/integrations";
|
||||
import { disconnectAnalyticsIntegrationActionFactory } from "~/actions/definitions/integrations";
|
||||
import Flex from "~/components/Flex";
|
||||
import styled from "styled-components";
|
||||
|
||||
@@ -106,7 +106,7 @@ function GoogleAnalytics() {
|
||||
</StyledSubmit>
|
||||
|
||||
<Button
|
||||
action={disconnectAnalyticsIntegrationFactory(integration)}
|
||||
action={disconnectAnalyticsIntegrationActionFactory(integration)}
|
||||
disabled={formState.isSubmitting}
|
||||
neutral
|
||||
hideIcon
|
||||
|
||||
@@ -15,7 +15,7 @@ import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import Icon from "./Icon";
|
||||
import { disconnectAnalyticsIntegrationFactory } from "~/actions/definitions/integrations";
|
||||
import { disconnectAnalyticsIntegrationActionFactory } from "~/actions/definitions/integrations";
|
||||
import Flex from "~/components/Flex";
|
||||
import styled from "styled-components";
|
||||
|
||||
@@ -127,7 +127,7 @@ function Matomo() {
|
||||
</StyledSubmit>
|
||||
|
||||
<Button
|
||||
action={disconnectAnalyticsIntegrationFactory(integration)}
|
||||
action={disconnectAnalyticsIntegrationActionFactory(integration)}
|
||||
disabled={formState.isSubmitting}
|
||||
neutral
|
||||
hideIcon
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { EditIcon, TrashIcon } from "outline-icons";
|
||||
import { TrashIcon } from "outline-icons";
|
||||
import ListItem from "~/components/List/Item";
|
||||
import Text from "~/components/Text";
|
||||
import { DropdownMenu } from "~/components/Menu/DropdownMenu";
|
||||
import { OverflowMenuButton } from "~/components/Menu/OverflowMenuButton";
|
||||
import { ActionSeparator, createAction } from "~/actions";
|
||||
import { renameActionFactory } from "~/actions/definitions/common";
|
||||
import { SettingsSection } from "~/actions/sections";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
import PasskeyIcon from "./PasskeyIcon";
|
||||
import { dateLocale, dateToRelative } from "@shared/utils/date";
|
||||
@@ -36,17 +38,12 @@ function PasskeyMenu({ onRename, onDelete }: Props) {
|
||||
|
||||
const actions = React.useMemo(
|
||||
() => [
|
||||
createAction({
|
||||
name: `${t("Rename")}…`,
|
||||
icon: <EditIcon />,
|
||||
section: "Passkey",
|
||||
perform: onRename,
|
||||
}),
|
||||
renameActionFactory({ section: SettingsSection, onRename }),
|
||||
ActionSeparator,
|
||||
createAction({
|
||||
name: `${t("Delete")}…`,
|
||||
icon: <TrashIcon />,
|
||||
section: "Passkey",
|
||||
section: SettingsSection,
|
||||
dangerous: true,
|
||||
perform: onDelete,
|
||||
}),
|
||||
|
||||
@@ -16,7 +16,7 @@ import Text from "~/components/Text";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import Icon from "./Icon";
|
||||
import Flex from "~/components/Flex";
|
||||
import { disconnectAnalyticsIntegrationFactory } from "~/actions/definitions/integrations";
|
||||
import { disconnectAnalyticsIntegrationActionFactory } from "~/actions/definitions/integrations";
|
||||
import styled from "styled-components";
|
||||
|
||||
type FormData = {
|
||||
@@ -147,7 +147,7 @@ function Umami() {
|
||||
</StyledSubmit>
|
||||
|
||||
<Button
|
||||
action={disconnectAnalyticsIntegrationFactory(integration)}
|
||||
action={disconnectAnalyticsIntegrationActionFactory(integration)}
|
||||
disabled={formState.isSubmitting}
|
||||
neutral
|
||||
hideIcon
|
||||
|
||||
@@ -1,84 +1,61 @@
|
||||
import { EditIcon, PlusIcon, TrashIcon } from "outline-icons";
|
||||
import stores from "~/stores";
|
||||
import type WebhookSubscription from "~/models/WebhookSubscription";
|
||||
import { createAction } from "~/actions";
|
||||
import { dialogActionFactory } from "~/actions/definitions/common";
|
||||
import { SettingsSection } from "~/actions/sections";
|
||||
import WebhookSubscriptionDeleteDialog from "./components/WebhookSubscriptionDeleteDialog";
|
||||
import WebhookSubscriptionEdit from "./components/WebhookSubscriptionEdit";
|
||||
import WebhookSubscriptionNew from "./components/WebhookSubscriptionNew";
|
||||
|
||||
export const createWebhookSubscription = createAction({
|
||||
name: ({ t }) => t("New webhook"),
|
||||
export const createWebhookSubscription = dialogActionFactory({
|
||||
analyticsName: "New webhook",
|
||||
section: SettingsSection,
|
||||
name: (t) => t("New webhook"),
|
||||
title: (t) => t("New webhook"),
|
||||
content: (onSubmit) => <WebhookSubscriptionNew onSubmit={onSubmit} />,
|
||||
icon: <PlusIcon />,
|
||||
keywords: "create",
|
||||
stopEvent: true,
|
||||
visible: () =>
|
||||
stores.policies.abilities(stores.auth.team?.id || "")
|
||||
.createWebhookSubscription,
|
||||
perform: ({ t, event }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("New webhook"),
|
||||
content: (
|
||||
<WebhookSubscriptionNew onSubmit={stores.dialogs.closeAllModals} />
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const editWebhookSubscriptionFactory = ({
|
||||
export const editWebhookSubscriptionActionFactory = ({
|
||||
webhook,
|
||||
}: {
|
||||
webhook: WebhookSubscription;
|
||||
}) =>
|
||||
createAction({
|
||||
name: ({ t }) => `${t("Edit")}…`,
|
||||
dialogActionFactory({
|
||||
analyticsName: "Edit webhook",
|
||||
section: SettingsSection,
|
||||
name: (t) => `${t("Edit")}…`,
|
||||
title: (t) => t("Edit webhook"),
|
||||
content: (onSubmit) => (
|
||||
<WebhookSubscriptionEdit
|
||||
onSubmit={onSubmit}
|
||||
webhookSubscription={webhook}
|
||||
/>
|
||||
),
|
||||
icon: <EditIcon />,
|
||||
perform: ({ t, event }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Edit webhook"),
|
||||
content: (
|
||||
<WebhookSubscriptionEdit
|
||||
onSubmit={stores.dialogs.closeAllModals}
|
||||
webhookSubscription={webhook}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
stopEvent: true,
|
||||
});
|
||||
|
||||
export const deleteWebhookSubscriptionFactory = ({
|
||||
export const deleteWebhookSubscriptionActionFactory = ({
|
||||
webhook,
|
||||
}: {
|
||||
webhook: WebhookSubscription;
|
||||
}) =>
|
||||
createAction({
|
||||
name: ({ t }) => `${t("Delete")}…`,
|
||||
dialogActionFactory({
|
||||
analyticsName: "Delete webhook",
|
||||
section: SettingsSection,
|
||||
name: (t) => `${t("Delete")}…`,
|
||||
title: (t) => t("Delete webhook"),
|
||||
content: (onSubmit) => (
|
||||
<WebhookSubscriptionDeleteDialog onSubmit={onSubmit} webhook={webhook} />
|
||||
),
|
||||
icon: <TrashIcon />,
|
||||
keywords: "delete remove",
|
||||
dangerous: true,
|
||||
perform: ({ t, event }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Delete webhook"),
|
||||
content: (
|
||||
<WebhookSubscriptionDeleteDialog
|
||||
onSubmit={stores.dialogs.closeAllModals}
|
||||
webhook={webhook}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
stopEvent: true,
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useMemo } from "react";
|
||||
import type WebhookSubscription from "~/models/WebhookSubscription";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
import {
|
||||
deleteWebhookSubscriptionFactory,
|
||||
editWebhookSubscriptionFactory,
|
||||
deleteWebhookSubscriptionActionFactory,
|
||||
editWebhookSubscriptionActionFactory,
|
||||
} from "../actions";
|
||||
|
||||
/**
|
||||
@@ -17,8 +17,8 @@ export function useWebhookSubscriptionMenuActions(
|
||||
) {
|
||||
const actions = useMemo(
|
||||
() => [
|
||||
editWebhookSubscriptionFactory({ webhook }),
|
||||
deleteWebhookSubscriptionFactory({ webhook }),
|
||||
editWebhookSubscriptionActionFactory({ webhook }),
|
||||
deleteWebhookSubscriptionActionFactory({ webhook }),
|
||||
],
|
||||
[webhook]
|
||||
);
|
||||
|
||||
@@ -44,8 +44,11 @@
|
||||
"Mark as resolved": "Mark as resolved",
|
||||
"Thread resolved": "Thread resolved",
|
||||
"Mark as unresolved": "Mark as unresolved",
|
||||
"Copy link": "Copy link",
|
||||
"Link copied to clipboard": "Link copied to clipboard",
|
||||
"View reactions": "View reactions",
|
||||
"Reactions": "Reactions",
|
||||
"Rename": "Rename",
|
||||
"Copy ID": "Copy ID",
|
||||
"Clear IndexedDB cache": "Clear IndexedDB cache",
|
||||
"IndexedDB cache cleared": "IndexedDB cache cleared",
|
||||
@@ -77,8 +80,6 @@
|
||||
"Copy as text": "Copy as text",
|
||||
"Text copied to clipboard": "Text copied to clipboard",
|
||||
"Copy public link": "Copy public link",
|
||||
"Link copied to clipboard": "Link copied to clipboard",
|
||||
"Copy link": "Copy link",
|
||||
"Duplicate": "Duplicate",
|
||||
"Duplicate document": "Duplicate document",
|
||||
"Copy document": "Copy document",
|
||||
@@ -119,6 +120,12 @@
|
||||
"Apply template": "Apply template",
|
||||
"New emoji": "New emoji",
|
||||
"Upload emoji": "Upload emoji",
|
||||
"Replace": "Replace",
|
||||
"Replace image": "Replace image",
|
||||
"Delete Emoji": "Delete Emoji",
|
||||
"Members": "Members",
|
||||
"Edit group": "Edit group",
|
||||
"Delete group": "Delete group",
|
||||
"Disconnect": "Disconnect",
|
||||
"Disconnect analytics": "Disconnect analytics",
|
||||
"Home": "Home",
|
||||
@@ -144,10 +151,11 @@
|
||||
"Archive all notifications": "Archive all notifications",
|
||||
"Mark as read": "Mark as read",
|
||||
"Mark as unread": "Mark as unread",
|
||||
"Revoke {{ appName }}": "Revoke {{ appName }}",
|
||||
"New App": "New App",
|
||||
"New Application": "New Application",
|
||||
"Delete app": "Delete app",
|
||||
"This version of the document was deleted": "This version of the document was deleted",
|
||||
"Link copied": "Link copied",
|
||||
"HTML": "HTML",
|
||||
"PDF": "PDF",
|
||||
"Exporting": "Exporting",
|
||||
@@ -185,6 +193,15 @@
|
||||
"Promote to {{ role }}": "Promote to {{ role }}",
|
||||
"Demote to {{ role }}": "Demote to {{ role }}",
|
||||
"Update role": "Update role",
|
||||
"Change role": "Change role",
|
||||
"Change profile picture": "Change profile picture",
|
||||
"Change name": "Change name",
|
||||
"Change email": "Change email",
|
||||
"Suspend user": "Suspend user",
|
||||
"Resend invite": "Resend invite",
|
||||
"Invite was resent to {{ userName }}": "Invite was resent to {{ userName }}",
|
||||
"Revoke invite": "Revoke invite",
|
||||
"Activate user": "Activate user",
|
||||
"Delete user": "Delete user",
|
||||
"Collection": "Collection",
|
||||
"Collections": "Collections",
|
||||
@@ -210,7 +227,6 @@
|
||||
"Avatar of {{ name }}": "Avatar of {{ name }}",
|
||||
"Viewers": "Viewers",
|
||||
"Managers": "Managers",
|
||||
"Members": "Members",
|
||||
"Manage templates": "Manage templates",
|
||||
"Choose who can create and edit templates in this collection.": "Choose who can create and edit templates in this collection.",
|
||||
"Public document sharing": "Public document sharing",
|
||||
@@ -320,6 +336,8 @@
|
||||
"Choose a name": "Choose a name",
|
||||
"name can only contain lowercase letters, numbers, and underscores.": "name can only contain lowercase letters, numbers, and underscores.",
|
||||
"This emoji will be available as": "This emoji will be available as",
|
||||
"Emoji deleted": "Emoji deleted",
|
||||
"Are you sure you want to delete the <em>{{emojiName}}</em> emoji? You will no longer be able to use it in your documents or collections.": "Are you sure you want to delete the <em>{{emojiName}}</em> emoji? You will no longer be able to use it in your documents or collections.",
|
||||
"Emoji replaced": "Emoji replaced",
|
||||
"Upload a new image to replace the current one for <em>{{emojiName}}</em>. All existing uses of this emoji will be updated automatically.": "Upload a new image to replace the current one for <em>{{emojiName}}</em>. All existing uses of this emoji will be updated automatically.",
|
||||
"Module failed to load": "Module failed to load",
|
||||
@@ -584,7 +602,6 @@
|
||||
"Enable regex": "Enable regex",
|
||||
"Replace options": "Replace options",
|
||||
"Replacement": "Replacement",
|
||||
"Replace": "Replace",
|
||||
"Replace all": "Replace all",
|
||||
"Options": "Options",
|
||||
"Go to link": "Go to link",
|
||||
@@ -666,7 +683,6 @@
|
||||
"Align right": "Align right",
|
||||
"Full width": "Full width",
|
||||
"Download image": "Download image",
|
||||
"Replace image": "Replace image",
|
||||
"Edit image URL": "Edit image URL",
|
||||
"Default width": "Default width",
|
||||
"Distribute columns": "Distribute columns",
|
||||
@@ -684,13 +700,6 @@
|
||||
"Move right": "Move right",
|
||||
"Move up": "Move up",
|
||||
"Move down": "Move down",
|
||||
"Rename": "Rename",
|
||||
"Delete Emoji": "Delete Emoji",
|
||||
"Emoji deleted": "Emoji deleted",
|
||||
"I'm sure – Delete": "I'm sure – Delete",
|
||||
"Are you sure you want to delete the <em>{{emojiName}}</em> emoji? You will no longer be able to use it in your documents or collections.": "Are you sure you want to delete the <em>{{emojiName}}</em> emoji? You will no longer be able to use it in your documents or collections.",
|
||||
"Edit group": "Edit group",
|
||||
"Delete group": "Delete group",
|
||||
"Could not import file": "Could not import file",
|
||||
"Unsubscribed from document": "Unsubscribed from document",
|
||||
"Unsubscribed from collection": "Unsubscribed from collection",
|
||||
@@ -709,15 +718,6 @@
|
||||
"Embeds": "Embeds",
|
||||
"Configure which embed providers are available in the editor.": "Configure which embed providers are available in the editor.",
|
||||
"Install": "Install",
|
||||
"Change profile picture": "Change profile picture",
|
||||
"Change name": "Change name",
|
||||
"Change email": "Change email",
|
||||
"Suspend user": "Suspend user",
|
||||
"An error occurred while sending the invite": "An error occurred while sending the invite",
|
||||
"Change role": "Change role",
|
||||
"Resend invite": "Resend invite",
|
||||
"Revoke invite": "Revoke invite",
|
||||
"Activate user": "Activate user",
|
||||
"API key": "API key",
|
||||
"Show path to document": "Show path to document",
|
||||
"Comment options": "Comment options",
|
||||
@@ -731,10 +731,6 @@
|
||||
"New document in <em>{{ collectionName }}</em>": "New document in <em>{{ collectionName }}</em>",
|
||||
"New child document": "New child document",
|
||||
"Save in workspace": "Save in workspace",
|
||||
"Revoke {{ appName }}": "Revoke {{ appName }}",
|
||||
"Revoking": "Revoking",
|
||||
"Are you sure you want to revoke access?": "Are you sure you want to revoke access?",
|
||||
"Delete app": "Delete app",
|
||||
"Revision options": "Revision options",
|
||||
"Share options": "Share options",
|
||||
"Headings you add to the document will appear here": "Headings you add to the document will appear here",
|
||||
@@ -1188,6 +1184,7 @@
|
||||
"Restricted scope": "Restricted scope",
|
||||
"API key copied to clipboard": "API key copied to clipboard",
|
||||
"Copied": "Copied",
|
||||
"Revoking": "Revoking",
|
||||
"Are you sure you want to revoke the {{ tokenName }} token?": "Are you sure you want to revoke the {{ tokenName }} token?",
|
||||
"Key": "Key",
|
||||
"Created by": "Created by",
|
||||
@@ -1275,6 +1272,7 @@
|
||||
"Role": "Role",
|
||||
"Guest": "Guest",
|
||||
"Never used": "Never used",
|
||||
"Are you sure you want to revoke access?": "Are you sure you want to revoke access?",
|
||||
"Are you sure you want to delete the {{ appName }} application? This cannot be undone.": "Are you sure you want to delete the {{ appName }} application? This cannot be undone.",
|
||||
"Title": "Title",
|
||||
"Shared by": "Shared by",
|
||||
|
||||
Reference in New Issue
Block a user