Skip to content
This repository was archived by the owner on May 19, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { PageStateSegmentedControl } from "@/features/user/components/page-state
import MovePageModal from "@/features/page/components/move-page-modal.tsx";
import { useTimeAgo } from "@/hooks/use-time-ago.tsx";
import ShareModal from "@/features/share/components/share-modal.tsx";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import {
useWatchStatusQuery,
useWatchPageMutation,
Expand All @@ -58,6 +59,9 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
const toggleAside = useToggleAside();
const [yjsConnectionStatus] = useAtom(yjsConnectionStatusAtom);
const [editor, setEditor] = useAtom(pageEditorAtom);
const [workspace] = useAtom(workspaceAtom);
const workspaceSharingDisabled =
workspace?.settings?.sharing?.disabled === true;

useHotkeys(
[
Expand All @@ -71,7 +75,10 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
[
"mod+H",
() => {
const event = new CustomEvent("openFindAndReplaceDialogFromEditor", {});
const event = new CustomEvent(
"openFindAndReplaceDialogFromEditor",
{},
);
document.dispatchEvent(event);
},
],
Expand Down Expand Up @@ -100,7 +107,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {

{!readOnly && <PageStateSegmentedControl size="xs" />}

<ShareModal readOnly={readOnly} />
{!workspaceSharingDisabled && <ShareModal readOnly={readOnly} />}

<Tooltip label={t("Comments")} openDelay={250} withArrow>
<ActionIcon
Expand Down Expand Up @@ -186,12 +193,12 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
const openFindDialog = () => {
const event = new CustomEvent("openFindDialogFromEditor", {});
document.dispatchEvent(event);
}
};

const openFindAndReplaceDialog = () => {
const event = new CustomEvent("openFindAndReplaceDialogFromEditor", {});
document.dispatchEvent(event);
}
};

return (
<>
Expand Down Expand Up @@ -250,7 +257,6 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
{t("Replace")}
</Menu.Item>


{watchStatus?.watching ? (
<Menu.Item
leftSection={<IconEyeOff size={16} />}
Expand Down
94 changes: 85 additions & 9 deletions apps/client/src/pages/settings/shares/shares.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,38 @@ import { Helmet } from "react-helmet-async";
import { getAppName } from "@/lib/config.ts";
import { useTranslation } from "react-i18next";
import ShareList from "@/features/share/components/share-list.tsx";
import { Alert, Text } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
import React from "react";
import { Alert, Switch, Text, Divider, Group } from "@mantine/core";
import { IconInfoCircle, IconWorld } from "@tabler/icons-react";
import React, { useState } from "react";
import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
import { notifications } from "@mantine/notifications";
import useUserRole from "@/hooks/use-user-role.tsx";

export default function Shares() {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [workspace, setWorkspace] = useAtom(workspaceAtom);
const { isAdmin } = useUserRole();
const sharingDisabled = workspace?.settings?.sharing?.disabled === true;

async function handleSharingToggle(checked: boolean) {
setIsLoading(true);
try {
const updatedWorkspace = await updateWorkspace({
disablePublicSharing: !checked,
});
setWorkspace(updatedWorkspace);
notifications.show({ message: t("Updated successfully") });
} catch (err) {
notifications.show({
message: t("Failed to update data"),
color: "red",
});
}
setIsLoading(false);
}

return (
<>
Expand All @@ -19,13 +45,63 @@ export default function Shares() {
</Helmet>
<SettingsTitle title={t("Public sharing")} />

<Alert variant="light" color="blue" icon={<IconInfoCircle />}>
{t(
"Publicly shared pages from spaces you are a member of will appear here",
)}
</Alert>
{isAdmin && (
<>
<div
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "space-between",
alignItems: "center",
gap: "1rem",
}}
>
<div style={{ flex: "1 1 300px", minWidth: 0 }}>
<Text fw={500}>{t("Allow public sharing")}</Text>
<Text size="sm" c="dimmed">
{t(
"When enabled, users can share pages publicly via a link. When disabled, existing shared links become inactive.",
)}
</Text>
</div>
<div style={{ flex: "0 0 auto" }}>
<Switch
aria-label={t("Toggle public sharing")}
checked={!sharingDisabled}
onChange={(event) =>
handleSharingToggle(event.currentTarget.checked)
}
disabled={isLoading || !isAdmin}
/>
</div>
</div>

<Divider my="lg" />
</>
)}

{sharingDisabled ? (
<Alert variant="light" color="red" icon={<IconWorld />}>
<Text fw={500} mb={4}>
{t("Public sharing is disabled")}
</Text>
<Text size="sm">
{t(
"Public sharing has been disabled at the workspace level. Existing shared links are inactive.",
)}
</Text>
</Alert>
) : (
<>
<Alert variant="light" color="blue" icon={<IconInfoCircle />}>
{t(
"Publicly shared pages from spaces you are a member of will appear here",
)}
</Alert>

<ShareList />
<ShareList />
</>
)}
</>
);
}
41 changes: 24 additions & 17 deletions apps/server/src/core/workspace/services/workspace.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ import {
generateRandomSuffixNumbers,
hashPassword,
} from '../../../common/helpers';
import { ChangePasswordDto, ChangeWorkspaceMemberPasswordDto } from '../../auth/dto/change-password.dto';
import {
ChangePasswordDto,
ChangeWorkspaceMemberPasswordDto,
} from '../../auth/dto/change-password.dto';
import ChangePasswordEmail from '@docmost/transactional/emails/change-password-email';
import ChangeUserPasswordEmail from '@docmost/transactional/emails/change-user-password-email';
import { MailService } from '../../../integrations/mail/mail.service';
Expand Down Expand Up @@ -96,7 +99,15 @@ export class WorkspaceService {
async getWorkspacePublicData(workspaceId: string) {
const workspace = await this.db
.selectFrom('workspaces')
.select(['id', 'name', 'logo', 'hostname', 'enforceSso', 'licenseKey', 'plan'])
.select([
'id',
'name',
'logo',
'hostname',
'enforceSso',
'licenseKey',
'plan',
])
.select((eb) =>
jsonArrayFrom(
eb
Expand Down Expand Up @@ -427,9 +438,6 @@ export class WorkspaceService {
updateWorkspaceDto.disablePublicSharing,
trx,
);
if (updateWorkspaceDto.disablePublicSharing) {
await this.shareRepo.deleteByWorkspaceId(workspaceId, trx);
}
}

if (typeof updateWorkspaceDto.mcpEnabled !== 'undefined') {
Expand Down Expand Up @@ -483,13 +491,7 @@ export class WorkspaceService {
});

const columnChanges = diffAuditTrackedFields(
[
'name',
'logo',
'enforceSso',
'enforceMfa',
'emailDomains',
],
['name', 'logo', 'enforceSso', 'enforceMfa', 'emailDomains'],
updateWorkspaceDto,
workspaceBefore,
workspace,
Expand Down Expand Up @@ -520,7 +522,11 @@ export class WorkspaceService {
if (user.role === 'owner' || user.role === 'admin') {
return this.userRepo.getUsersPaginated(workspaceId, pagination);
}
return this.userRepo.getUsersInSpacesOfUser(workspaceId, user.id, pagination);
return this.userRepo.getUsersInSpacesOfUser(
workspaceId,
user.id,
pagination,
);
}

async updateWorkspaceUserRole(
Expand Down Expand Up @@ -825,9 +831,7 @@ export class WorkspaceService {
throw new NotFoundException('User not found');
}

if (
(actorUser.role === UserRole.ADMIN && user.role === UserRole.OWNER)
) {
if (actorUser.role === UserRole.ADMIN && user.role === UserRole.OWNER) {
throw new ForbiddenException();
}

Expand All @@ -850,7 +854,10 @@ export class WorkspaceService {
workspaceId,
);

const emailTemplate = ChangeUserPasswordEmail({ username: user.name, actorUsername: actorUser.name });
const emailTemplate = ChangeUserPasswordEmail({
username: user.name,
actorUsername: actorUser.name,
});
await this.mailService.sendToQueue({
to: user.email,
subject: 'Your password has been changed',
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/database/types/entity.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ export type Graph = {
title: string | null;
parentPageId: string | null;
backlinks: GraphBacklink[];
}
};
export type GraphBacklink = {
sourcePageId: string;
targetPageId: string;
}
};

// SpaceMember
export type SpaceMember = Selectable<SpaceMembers>;
Expand Down
Loading