mirror of
https://github.com/docmost/docmost.git
synced 2026-05-14 04:24:04 +08:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe3732add9 | |||
| 9db9eb852c | |||
| 3007060ac4 | |||
| ba9e58ede9 | |||
| f2bc0b5049 | |||
| eba5ae2eb3 | |||
| b6344f2e08 | |||
| db52b6036e | |||
| fe5b236d41 | |||
| b3c5ca6d5f | |||
| 483e39db1c | |||
| 56f476649c | |||
| 1b82959859 | |||
| 4d322e9157 | |||
| 20a7acfccc | |||
| 040ad04a27 | |||
| d3ca1ed72c | |||
| 207b1b593a | |||
| 6c664a366f | |||
| 4873f7b9ff | |||
| 718ca2b674 | |||
| e189d01ce0 | |||
| edd3754e46 | |||
| b2b147f1bd | |||
| 129e21f728 | |||
| 70124475ab | |||
| 7d7decb459 | |||
| 1eba6e93cc | |||
| cd52acc415 | |||
| a09e35ba8f | |||
| 8e2cf9bb02 | |||
| cff4ba8e50 | |||
| f44bb3f121 | |||
| 669ff0435f |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.25.1",
|
"version": "0.25.0-beta.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "Seite",
|
"page": "Seite",
|
||||||
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
||||||
"Page history": "Seitengeschichte",
|
"Page history": "Seitengeschichte",
|
||||||
"Select version": "Version auswählen",
|
|
||||||
"Highlight changes": "Änderungen hervorheben",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.",
|
"Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.",
|
||||||
"Pages": "Seiten",
|
"Pages": "Seiten",
|
||||||
"pages": "Seiten",
|
"pages": "Seiten",
|
||||||
|
|||||||
@@ -123,7 +123,10 @@
|
|||||||
"page": "page",
|
"page": "page",
|
||||||
"Page deleted successfully": "Page deleted successfully",
|
"Page deleted successfully": "Page deleted successfully",
|
||||||
"Page history": "Page history",
|
"Page history": "Page history",
|
||||||
|
"Version history for": "Version history for",
|
||||||
|
"document": "document",
|
||||||
"Select version": "Select version",
|
"Select version": "Select version",
|
||||||
|
"Close": "Close",
|
||||||
"Highlight changes": "Highlight changes",
|
"Highlight changes": "Highlight changes",
|
||||||
"Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.",
|
"Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.",
|
||||||
"Pages": "Pages",
|
"Pages": "Pages",
|
||||||
@@ -407,21 +410,6 @@
|
|||||||
"Share deleted successfully": "Share deleted successfully",
|
"Share deleted successfully": "Share deleted successfully",
|
||||||
"Share not found": "Share not found",
|
"Share not found": "Share not found",
|
||||||
"Failed to share page": "Failed to share page",
|
"Failed to share page": "Failed to share page",
|
||||||
"Disable public sharing": "Disable public sharing",
|
|
||||||
"Prevent members from sharing pages publicly.": "Prevent members from sharing pages publicly.",
|
|
||||||
"Toggle public sharing": "Toggle public sharing",
|
|
||||||
"Toggle space public sharing": "Toggle space public sharing",
|
|
||||||
"Public sharing is disabled at the workspace level": "Public sharing is disabled at the workspace level",
|
|
||||||
"Prevent pages in this space from being shared publicly.": "Prevent pages in this space from being shared publicly.",
|
|
||||||
"Requires an enterprise license": "Requires an enterprise license",
|
|
||||||
"Enable public sharing": "Enable public sharing",
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Are you sure you want to enable public sharing? Members will be able to share pages publicly.",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.",
|
|
||||||
"Are you sure you want to enable public sharing for this space?": "Are you sure you want to enable public sharing for this space?",
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.",
|
|
||||||
"Public sharing is disabled": "Public sharing is disabled",
|
|
||||||
"Public sharing has been disabled at the workspace level.": "Public sharing has been disabled at the workspace level.",
|
|
||||||
"Public sharing has been disabled for this space.": "Public sharing has been disabled for this space.",
|
|
||||||
"Copy page": "Copy page",
|
"Copy page": "Copy page",
|
||||||
"Copy page to a different space.": "Copy page to a different space.",
|
"Copy page to a different space.": "Copy page to a different space.",
|
||||||
"Page copied successfully": "Page copied successfully",
|
"Page copied successfully": "Page copied successfully",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "página",
|
"page": "página",
|
||||||
"Page deleted successfully": "Página eliminada con éxito",
|
"Page deleted successfully": "Página eliminada con éxito",
|
||||||
"Page history": "Historial de la página",
|
"Page history": "Historial de la página",
|
||||||
"Select version": "Seleccionar versión",
|
|
||||||
"Highlight changes": "Resaltar cambios",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "La importación de la página está en curso. Por favor, no cierre esta pestaña.",
|
"Page import is in progress. Please do not close this tab.": "La importación de la página está en curso. Por favor, no cierre esta pestaña.",
|
||||||
"Pages": "Páginas",
|
"Pages": "Páginas",
|
||||||
"pages": "páginas",
|
"pages": "páginas",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "page",
|
"page": "page",
|
||||||
"Page deleted successfully": "Page supprimée avec succès",
|
"Page deleted successfully": "Page supprimée avec succès",
|
||||||
"Page history": "Historique de la page",
|
"Page history": "Historique de la page",
|
||||||
"Select version": "Sélectionner la version",
|
|
||||||
"Highlight changes": "Mettre en évidence les changements",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "L'importation de la page est en cours. Veuillez ne pas fermer cet onglet.",
|
"Page import is in progress. Please do not close this tab.": "L'importation de la page est en cours. Veuillez ne pas fermer cet onglet.",
|
||||||
"Pages": "Pages",
|
"Pages": "Pages",
|
||||||
"pages": "pages",
|
"pages": "pages",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "pagina",
|
"page": "pagina",
|
||||||
"Page deleted successfully": "Pagina eliminata con successo",
|
"Page deleted successfully": "Pagina eliminata con successo",
|
||||||
"Page history": "Cronologia della pagina",
|
"Page history": "Cronologia della pagina",
|
||||||
"Select version": "Seleziona versione",
|
|
||||||
"Highlight changes": "Evidenzia modifiche",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "L'importazione della pagina è in corso. Si prega di non chiudere questa scheda.",
|
"Page import is in progress. Please do not close this tab.": "L'importazione della pagina è in corso. Si prega di non chiudere questa scheda.",
|
||||||
"Pages": "Pagine",
|
"Pages": "Pagine",
|
||||||
"pages": "pagine",
|
"pages": "pagine",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "ページ",
|
"page": "ページ",
|
||||||
"Page deleted successfully": "ページを削除しました",
|
"Page deleted successfully": "ページを削除しました",
|
||||||
"Page history": "ページ履歴",
|
"Page history": "ページ履歴",
|
||||||
"Select version": "バージョンを選択",
|
|
||||||
"Highlight changes": "変更を強調表示",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "ページをインポート中です。このタブを閉じないでください",
|
"Page import is in progress. Please do not close this tab.": "ページをインポート中です。このタブを閉じないでください",
|
||||||
"Pages": "ページ",
|
"Pages": "ページ",
|
||||||
"pages": "ページ",
|
"pages": "ページ",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "페이지",
|
"page": "페이지",
|
||||||
"Page deleted successfully": "페이지 삭제 완료",
|
"Page deleted successfully": "페이지 삭제 완료",
|
||||||
"Page history": "페이지 기록",
|
"Page history": "페이지 기록",
|
||||||
"Select version": "버전 선택",
|
|
||||||
"Highlight changes": "변경 사항 강조",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "페이지 가져오기가 진행 중입니다. 이 탭을 닫지 마세요.",
|
"Page import is in progress. Please do not close this tab.": "페이지 가져오기가 진행 중입니다. 이 탭을 닫지 마세요.",
|
||||||
"Pages": "페이지",
|
"Pages": "페이지",
|
||||||
"pages": "페이지",
|
"pages": "페이지",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "pagina",
|
"page": "pagina",
|
||||||
"Page deleted successfully": "Pagina succesvol verwijderd",
|
"Page deleted successfully": "Pagina succesvol verwijderd",
|
||||||
"Page history": "Pagina geschiedenis",
|
"Page history": "Pagina geschiedenis",
|
||||||
"Select version": "Selecteer versie",
|
|
||||||
"Highlight changes": "Wijzigingen markeren",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "Importeren van pagina's is bezig. Sluit dit tabblad niet.",
|
"Page import is in progress. Please do not close this tab.": "Importeren van pagina's is bezig. Sluit dit tabblad niet.",
|
||||||
"Pages": "Pagina's",
|
"Pages": "Pagina's",
|
||||||
"pages": "pagina's",
|
"pages": "pagina's",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "página",
|
"page": "página",
|
||||||
"Page deleted successfully": "Página excluída com sucesso",
|
"Page deleted successfully": "Página excluída com sucesso",
|
||||||
"Page history": "Histórico da página",
|
"Page history": "Histórico da página",
|
||||||
"Select version": "Selecionar versão",
|
|
||||||
"Highlight changes": "Destacar alterações",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "A importação da página está em andamento. Por favor, não feche esta aba.",
|
"Page import is in progress. Please do not close this tab.": "A importação da página está em andamento. Por favor, não feche esta aba.",
|
||||||
"Pages": "Páginas",
|
"Pages": "Páginas",
|
||||||
"pages": "páginas",
|
"pages": "páginas",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "страница",
|
"page": "страница",
|
||||||
"Page deleted successfully": "Страница успешно удалена",
|
"Page deleted successfully": "Страница успешно удалена",
|
||||||
"Page history": "История страницы",
|
"Page history": "История страницы",
|
||||||
"Select version": "Выбрать версию",
|
|
||||||
"Highlight changes": "Выделить изменения",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "Импорт страницы в процессе. Пожалуйста, не закрывайте эту вкладку.",
|
"Page import is in progress. Please do not close this tab.": "Импорт страницы в процессе. Пожалуйста, не закрывайте эту вкладку.",
|
||||||
"Pages": "Страницы",
|
"Pages": "Страницы",
|
||||||
"pages": "страницы",
|
"pages": "страницы",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "сторінка",
|
"page": "сторінка",
|
||||||
"Page deleted successfully": "Сторінку успішно видалено",
|
"Page deleted successfully": "Сторінку успішно видалено",
|
||||||
"Page history": "Історія сторінки",
|
"Page history": "Історія сторінки",
|
||||||
"Select version": "Вибрати версію",
|
|
||||||
"Highlight changes": "Підсвітити зміни",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "Імпорт сторінки в процесі. Будь ласка, не закривайте цю вкладку.",
|
"Page import is in progress. Please do not close this tab.": "Імпорт сторінки в процесі. Будь ласка, не закривайте цю вкладку.",
|
||||||
"Pages": "Сторінки",
|
"Pages": "Сторінки",
|
||||||
"pages": "сторінки",
|
"pages": "сторінки",
|
||||||
|
|||||||
@@ -123,8 +123,6 @@
|
|||||||
"page": "个页面",
|
"page": "个页面",
|
||||||
"Page deleted successfully": "页面已成功删除",
|
"Page deleted successfully": "页面已成功删除",
|
||||||
"Page history": "页面历史",
|
"Page history": "页面历史",
|
||||||
"Select version": "选择版本",
|
|
||||||
"Highlight changes": "突出显示更改",
|
|
||||||
"Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。",
|
"Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。",
|
||||||
"Pages": "页面",
|
"Pages": "页面",
|
||||||
"pages": "个页面",
|
"pages": "个页面",
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { isCloud } from "@/lib/config";
|
|
||||||
import useLicense from "@/ee/hooks/use-license";
|
|
||||||
import usePlan from "@/ee/hooks/use-plan";
|
|
||||||
|
|
||||||
const useEnterpriseAccess = () => {
|
|
||||||
const { hasLicenseKey } = useLicense();
|
|
||||||
const { isBusiness } = usePlan();
|
|
||||||
|
|
||||||
return (isCloud() && isBusiness) || (!isCloud() && hasLicenseKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useEnterpriseAccess;
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import { Group, Text, Switch, Tooltip } from "@mantine/core";
|
|
||||||
import { modals } from "@mantine/modals";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
|
||||||
import { notifications } from "@mantine/notifications";
|
|
||||||
import useEnterpriseAccess from "@/ee/hooks/use-enterprise-access.tsx";
|
|
||||||
|
|
||||||
export default function DisablePublicSharing() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
|
||||||
<div>
|
|
||||||
<Text size="md">{t("Disable public sharing")}</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{t("Prevent members from sharing pages publicly.")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DisablePublicSharingToggle />
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DisablePublicSharingToggle() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
|
||||||
const [checked, setChecked] = useState(
|
|
||||||
workspace?.settings?.sharing?.disabled === true,
|
|
||||||
);
|
|
||||||
const hasAccess = useEnterpriseAccess();
|
|
||||||
|
|
||||||
const applyChange = async (value: boolean) => {
|
|
||||||
try {
|
|
||||||
const updatedWorkspace = await updateWorkspace({
|
|
||||||
disablePublicSharing: value,
|
|
||||||
});
|
|
||||||
setChecked(value);
|
|
||||||
setWorkspace(updatedWorkspace);
|
|
||||||
} catch (err) {
|
|
||||||
notifications.show({
|
|
||||||
message: err?.response?.data?.message,
|
|
||||||
color: "red",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = event.currentTarget.checked;
|
|
||||||
|
|
||||||
modals.openConfirmModal({
|
|
||||||
title: value ? t("Disable public sharing") : t("Enable public sharing"),
|
|
||||||
children: (
|
|
||||||
<Text size="sm">
|
|
||||||
{value
|
|
||||||
? t(
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.",
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.",
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
centered: true,
|
|
||||||
labels: { confirm: t("Confirm"), cancel: t("Cancel") },
|
|
||||||
confirmProps: value ? { color: "red" } : {},
|
|
||||||
onConfirm: () => applyChange(value),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
label={t("Requires an enterprise license")}
|
|
||||||
disabled={hasAccess}
|
|
||||||
refProp="rootRef"
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
checked={checked}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!hasAccess}
|
|
||||||
aria-label={t("Toggle public sharing")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -10,18 +10,23 @@ export default function EnforceMfa() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
<>
|
||||||
<div>
|
<Title order={4} my="sm">
|
||||||
<Text size="md">{t("Enforce two-factor authentication")}</Text>
|
MFA
|
||||||
<Text size="sm" c="dimmed">
|
</Title>
|
||||||
{t(
|
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||||
"Once enforced, all members must enable two-factor authentication to access the workspace.",
|
<div>
|
||||||
)}
|
<Text size="md">{t("Enforce two-factor authentication")}</Text>
|
||||||
</Text>
|
<Text size="sm" c="dimmed">
|
||||||
</div>
|
{t(
|
||||||
|
"Once enforced, all members must enable two-factor authentication to access the workspace.",
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
<EnforceMfaToggle />
|
<EnforceMfaToggle />
|
||||||
</Group>
|
</Group>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import { Group, Text, Switch, Tooltip } from "@mantine/core";
|
|
||||||
import { modals } from "@mantine/modals";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { ISpace } from "@/features/space/types/space.types.ts";
|
|
||||||
import { useUpdateSpaceMutation } from "@/features/space/queries/space-query.ts";
|
|
||||||
|
|
||||||
type SpacePublicSharingToggleProps = {
|
|
||||||
space: ISpace;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SpacePublicSharingToggle({
|
|
||||||
space,
|
|
||||||
}: SpacePublicSharingToggleProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [workspace] = useAtom(workspaceAtom);
|
|
||||||
const workspaceDisabled = workspace?.settings?.sharing?.disabled === true;
|
|
||||||
const [checked, setChecked] = useState(
|
|
||||||
space.settings?.sharing?.disabled === true,
|
|
||||||
);
|
|
||||||
const updateSpaceMutation = useUpdateSpaceMutation();
|
|
||||||
|
|
||||||
const applyChange = async (value: boolean) => {
|
|
||||||
try {
|
|
||||||
await updateSpaceMutation.mutateAsync({
|
|
||||||
spaceId: space.id,
|
|
||||||
disablePublicSharing: value,
|
|
||||||
});
|
|
||||||
setChecked(value);
|
|
||||||
} catch {
|
|
||||||
// error handled by mutation
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = event.currentTarget.checked;
|
|
||||||
|
|
||||||
modals.openConfirmModal({
|
|
||||||
title: value ? t("Disable public sharing") : t("Enable public sharing"),
|
|
||||||
children: (
|
|
||||||
<Text size="sm">
|
|
||||||
{value
|
|
||||||
? t(
|
|
||||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.",
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"Are you sure you want to enable public sharing for this space?",
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
centered: true,
|
|
||||||
labels: { confirm: t("Confirm"), cancel: t("Cancel") },
|
|
||||||
confirmProps: value ? { color: "red" } : {},
|
|
||||||
onConfirm: () => applyChange(value),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
|
||||||
<div>
|
|
||||||
<Text size="md">{t("Disable public sharing")}</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{workspaceDisabled
|
|
||||||
? t("Public sharing is disabled at the workspace level")
|
|
||||||
: t("Prevent pages in this space from being shared publicly.")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Tooltip
|
|
||||||
label={t("Public sharing is disabled at the workspace level")}
|
|
||||||
disabled={!workspaceDisabled}
|
|
||||||
refProp="rootRef"
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
checked={checked}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={workspaceDisabled}
|
|
||||||
aria-label={t("Toggle space public sharing")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -9,16 +9,15 @@ import CreateSsoProvider from "@/ee/security/components/create-sso-provider.tsx"
|
|||||||
import EnforceSso from "@/ee/security/components/enforce-sso.tsx";
|
import EnforceSso from "@/ee/security/components/enforce-sso.tsx";
|
||||||
import AllowedDomains from "@/ee/security/components/allowed-domains.tsx";
|
import AllowedDomains from "@/ee/security/components/allowed-domains.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import useLicense from "@/ee/hooks/use-license.tsx";
|
||||||
|
import usePlan from "@/ee/hooks/use-plan.tsx";
|
||||||
import EnforceMfa from "@/ee/security/components/enforce-mfa.tsx";
|
import EnforceMfa from "@/ee/security/components/enforce-mfa.tsx";
|
||||||
import DisablePublicSharing from "@/ee/security/components/disable-public-sharing.tsx";
|
|
||||||
import useEnterpriseAccess from "@/ee/hooks/use-enterprise-access.tsx";
|
|
||||||
import { useIsCloudEE } from "@/hooks/use-is-cloud-ee.tsx";
|
|
||||||
|
|
||||||
export default function Security() {
|
export default function Security() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isAdmin } = useUserRole();
|
const { isAdmin } = useUserRole();
|
||||||
const hasEnterpriseAccess = useEnterpriseAccess();
|
const { hasLicenseKey } = useLicense();
|
||||||
const isCloudEE = useIsCloudEE();
|
const { isBusiness } = usePlan();
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return null;
|
return null;
|
||||||
@@ -31,41 +30,26 @@ export default function Security() {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
<SettingsTitle title={t("Security")} />
|
<SettingsTitle title={t("Security")} />
|
||||||
|
|
||||||
<EnforceMfa />
|
<AllowedDomains />
|
||||||
|
|
||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
|
|
||||||
{(!isCloud() || hasEnterpriseAccess) && (
|
<EnforceMfa />
|
||||||
<>
|
|
||||||
<DisablePublicSharing />
|
<Divider my="lg" />
|
||||||
<Divider my="lg" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Title order={4} my="lg">
|
<Title order={4} my="lg">
|
||||||
Single sign-on (SSO)
|
Single sign-on (SSO)
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
{hasEnterpriseAccess && (
|
{(isCloud() && isBusiness) || (!isCloud() && hasLicenseKey) ? (
|
||||||
<>
|
<>
|
||||||
<EnforceSso />
|
<EnforceSso />
|
||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isCloudEE && (
|
|
||||||
<>
|
|
||||||
<AllowedDomains />
|
|
||||||
<Divider my="lg" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasEnterpriseAccess && (
|
|
||||||
<>
|
|
||||||
<CreateSsoProvider />
|
<CreateSsoProvider />
|
||||||
<Divider size={0} my="lg" />
|
<Divider size={0} my="lg" />
|
||||||
</>
|
</>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
<SsoProviderList />
|
<SsoProviderList />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|||||||
import { EditorProvider } from "@tiptap/react";
|
import { EditorProvider } from "@tiptap/react";
|
||||||
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
||||||
import { Document } from "@tiptap/extension-document";
|
import { Document } from "@tiptap/extension-document";
|
||||||
import { Heading, UniqueID } from "@docmost/editor-ext";
|
import { Heading, generateNodeId, UniqueID } from "@docmost/editor-ext";
|
||||||
import { Text } from "@tiptap/extension-text";
|
import { Text } from "@tiptap/extension-text";
|
||||||
import { Placeholder } from "@tiptap/extension-placeholder";
|
import { Placeholder } from "@tiptap/extension-placeholder";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mantine-AppShell-main {
|
.mantine-AppShell-main {
|
||||||
padding: 0 !important;
|
padding-top: 0 !important;
|
||||||
min-height: auto !important;
|
min-height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
.diffSummary {
|
||||||
|
border: rem(1px) solid
|
||||||
|
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||||
|
border-radius: rem(10px);
|
||||||
|
padding: rem(12px);
|
||||||
|
background: light-dark(
|
||||||
|
var(--mantine-color-gray-0),
|
||||||
|
var(--mantine-color-dark-7)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-added) {
|
||||||
|
background: light-dark(#e1f3f2, #01654a) !important;
|
||||||
|
color: light-dark(#007b69, #cafff7) !important;
|
||||||
|
-webkit-box-decoration-break: clone;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-deleted) {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: light-dark(var(--mantine-color-red-7), var(--mantine-color-red-4));
|
||||||
|
background: light-dark(var(--mantine-color-red-0), rgba(255, 0, 0, 0.1));
|
||||||
|
border-radius: rem(2px);
|
||||||
|
padding: 0 rem(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-node-added) {
|
||||||
|
outline: rem(2px) solid light-dark(var(--mantine-color-teal-5), var(--mantine-color-teal-7));
|
||||||
|
outline-offset: rem(2px);
|
||||||
|
border-radius: rem(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-node-deleted) {
|
||||||
|
opacity: 0.5;
|
||||||
|
outline: rem(2px) dashed light-dark(var(--mantine-color-red-4), var(--mantine-color-red-6));
|
||||||
|
outline-offset: rem(4px);
|
||||||
|
border-radius: rem(4px);
|
||||||
|
}
|
||||||
@@ -16,36 +16,6 @@
|
|||||||
:global(.ProseMirror) {
|
:global(.ProseMirror) {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
& :global(.history-diff-added) {
|
|
||||||
background: light-dark(#e1f3f2, #01654a) !important;
|
|
||||||
color: light-dark(#007b69, #cafff7) !important;
|
|
||||||
-webkit-box-decoration-break: clone;
|
|
||||||
box-decoration-break: clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
& :global(.history-diff-deleted) {
|
|
||||||
text-decoration: line-through;
|
|
||||||
color: light-dark(var(--mantine-color-red-7), var(--mantine-color-red-4));
|
|
||||||
background: light-dark(var(--mantine-color-red-0), rgba(255, 0, 0, 0.1));
|
|
||||||
border-radius: rem(2px);
|
|
||||||
padding: 0 rem(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
& :global(.history-diff-node-added) {
|
|
||||||
outline: rem(2px) solid
|
|
||||||
light-dark(var(--mantine-color-teal-5), var(--mantine-color-teal-7));
|
|
||||||
outline-offset: rem(2px);
|
|
||||||
border-radius: rem(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
& :global(.history-diff-node-deleted) {
|
|
||||||
opacity: 0.5;
|
|
||||||
outline: rem(2px) dashed
|
|
||||||
light-dark(var(--mantine-color-red-4), var(--mantine-color-red-6));
|
|
||||||
outline-offset: rem(4px);
|
|
||||||
border-radius: rem(4px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import "@/features/editor/styles/index.css";
|
import "@/features/editor/styles/index.css";
|
||||||
|
import "./css/history-diff.module.css";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { EditorContent, useEditor } from "@tiptap/react";
|
import { EditorContent, useEditor } from "@tiptap/react";
|
||||||
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
||||||
@@ -43,21 +44,21 @@ export function HistoryEditor({
|
|||||||
if (previousContent) {
|
if (previousContent) {
|
||||||
try {
|
try {
|
||||||
const schema = editor.schema;
|
const schema = editor.schema;
|
||||||
const oldContent = Node.fromJSON(schema, previousContent);
|
const docOld = Node.fromJSON(schema, previousContent);
|
||||||
const newContent = Node.fromJSON(schema, content);
|
const docNew = Node.fromJSON(schema, content);
|
||||||
|
|
||||||
const tr = recreateTransform(oldContent, newContent, {
|
const tr = recreateTransform(docOld, docNew, {
|
||||||
complexSteps: false,
|
complexSteps: true,
|
||||||
wordDiffs: true,
|
wordDiffs: true,
|
||||||
simplifyDiff: true,
|
simplifyDiff: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeSet = ChangeSet.create(oldContent).addSteps(
|
const changeSet = ChangeSet.create(docOld).addSteps(
|
||||||
tr.doc,
|
tr.doc,
|
||||||
tr.mapping.maps,
|
tr.mapping.maps,
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const changes = simplifyChanges(changeSet.changes, newContent);
|
const changes = simplifyChanges(changeSet.changes, docNew);
|
||||||
|
|
||||||
editor.commands.setContent(content);
|
editor.commands.setContent(content);
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ export function HistoryEditor({
|
|||||||
changeIndex++;
|
changeIndex++;
|
||||||
const currentIndex = changeIndex;
|
const currentIndex = changeIndex;
|
||||||
let foundSpecialNode: { node: Node; pos: number } | null = null;
|
let foundSpecialNode: { node: Node; pos: number } | null = null;
|
||||||
newContent.nodesBetween(change.fromB, change.toB, (node, pos) => {
|
docNew.nodesBetween(change.fromB, change.toB, (node, pos) => {
|
||||||
if (specialNodeTypes.has(node.type.name)) {
|
if (specialNodeTypes.has(node.type.name)) {
|
||||||
const nodeEnd = pos + node.nodeSize;
|
const nodeEnd = pos + node.nodeSize;
|
||||||
if (change.fromB <= pos && change.toB >= nodeEnd) {
|
if (change.fromB <= pos && change.toB >= nodeEnd) {
|
||||||
@@ -116,7 +117,7 @@ export function HistoryEditor({
|
|||||||
changeIndex++;
|
changeIndex++;
|
||||||
const currentIndex = changeIndex;
|
const currentIndex = changeIndex;
|
||||||
let foundDeletedNode: { node: Node; pos: number } | null = null;
|
let foundDeletedNode: { node: Node; pos: number } | null = null;
|
||||||
oldContent.nodesBetween(change.fromA, change.toA, (node, pos) => {
|
docOld.nodesBetween(change.fromA, change.toA, (node, pos) => {
|
||||||
if (specialNodeTypes.has(node.type.name)) {
|
if (specialNodeTypes.has(node.type.name)) {
|
||||||
const nodeEnd = pos + node.nodeSize;
|
const nodeEnd = pos + node.nodeSize;
|
||||||
if (change.fromA <= pos && change.toA >= nodeEnd) {
|
if (change.fromA <= pos && change.toA >= nodeEnd) {
|
||||||
@@ -139,7 +140,7 @@ export function HistoryEditor({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const deletedText = oldContent.textBetween(
|
const deletedText = docOld.textBetween(
|
||||||
change.fromA,
|
change.fromA,
|
||||||
change.toA,
|
change.toA,
|
||||||
"",
|
"",
|
||||||
@@ -160,7 +161,7 @@ export function HistoryEditor({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
decorationSet = DecorationSet.create(newContent, decorations);
|
decorationSet = DecorationSet.create(docNew, decorations);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("History diff failed:", e);
|
console.error("History diff failed:", e);
|
||||||
editor.commands.setContent(content);
|
editor.commands.setContent(content);
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ function HistoryList({ pageId }: Props) {
|
|||||||
size="compact-md"
|
size="compact-md"
|
||||||
onClick={() => setHistoryModalOpen(false)}
|
onClick={() => setHistoryModalOpen(false)}
|
||||||
>
|
>
|
||||||
{t("Cancel")}
|
{t("Close")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="compact-md" onClick={confirmRestore}>
|
<Button size="compact-md" onClick={confirmRestore}>
|
||||||
{t("Restore")}
|
{t("Restore")}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export default function HistoryModalMobile({ pageId, pageTitle }: Props) {
|
|||||||
{canRestore && (
|
{canRestore && (
|
||||||
<Group className={classes.actionButtons} justify="flex-end" gap="sm">
|
<Group className={classes.actionButtons} justify="flex-end" gap="sm">
|
||||||
<Button variant="default" onClick={() => setHistoryModalOpen(false)}>
|
<Button variant="default" onClick={() => setHistoryModalOpen(false)}>
|
||||||
{t("Cancel")}
|
{t("Close")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={confirmRestore}>{t("Restore")}</Button>
|
<Button onClick={confirmRestore}>{t("Restore")}</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ import { getAppUrl, isCloud } from "@/lib/config.ts";
|
|||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
import classes from "@/features/share/components/share.module.css";
|
import classes from "@/features/share/components/share.module.css";
|
||||||
import useTrial from "@/ee/hooks/use-trial.tsx";
|
import useTrial from "@/ee/hooks/use-trial.tsx";
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
|
||||||
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
|
|
||||||
|
|
||||||
interface ShareModalProps {
|
interface ShareModalProps {
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
@@ -43,12 +40,6 @@ export default function ShareModal({ readOnly }: ShareModalProps) {
|
|||||||
const { data: share } = useShareForPageQuery(pageId);
|
const { data: share } = useShareForPageQuery(pageId);
|
||||||
const { spaceSlug } = useParams();
|
const { spaceSlug } = useParams();
|
||||||
const { isTrial } = useTrial();
|
const { isTrial } = useTrial();
|
||||||
const [workspace] = useAtom(workspaceAtom);
|
|
||||||
const { data: space } = useSpaceQuery(spaceSlug);
|
|
||||||
const workspaceDisabled =
|
|
||||||
workspace?.settings?.sharing?.disabled === true;
|
|
||||||
const spaceDisabled = space?.settings?.sharing?.disabled === true;
|
|
||||||
const sharingDisabled = workspaceDisabled || spaceDisabled;
|
|
||||||
const createShareMutation = useCreateShareMutation();
|
const createShareMutation = useCreateShareMutation();
|
||||||
const updateShareMutation = useUpdateShareMutation();
|
const updateShareMutation = useUpdateShareMutation();
|
||||||
const deleteShareMutation = useDeleteShareMutation();
|
const deleteShareMutation = useDeleteShareMutation();
|
||||||
@@ -173,20 +164,6 @@ export default function ShareModal({ readOnly }: ShareModalProps) {
|
|||||||
{t("Upgrade Plan")}
|
{t("Upgrade Plan")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : sharingDisabled ? (
|
|
||||||
<>
|
|
||||||
<Group justify="center" mb="sm">
|
|
||||||
<IconLock size={20} stroke={1.5} />
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" ta="center" fw={500} mb="xs">
|
|
||||||
{t("Public sharing is disabled")}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
|
||||||
{workspaceDisabled
|
|
||||||
? t("Public sharing has been disabled at the workspace level.")
|
|
||||||
: t("Public sharing has been disabled for this space.")}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
) : isDescendantShared ? (
|
) : isDescendantShared ? (
|
||||||
<>
|
<>
|
||||||
<Text size="sm">{t("Inherits public sharing from")}</Text>
|
<Text size="sm">{t("Inherits public sharing from")}</Text>
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import {
|
|||||||
ResponsiveSettingsControl,
|
ResponsiveSettingsControl,
|
||||||
ResponsiveSettingsRow,
|
ResponsiveSettingsRow,
|
||||||
} from "@/components/ui/responsive-settings-row.tsx";
|
} from "@/components/ui/responsive-settings-row.tsx";
|
||||||
import SpacePublicSharingToggle from "@/ee/security/components/space-public-sharing-toggle.tsx";
|
|
||||||
import useEnterpriseAccess from "@/ee/hooks/use-enterprise-access.tsx";
|
|
||||||
|
|
||||||
interface SpaceDetailsProps {
|
interface SpaceDetailsProps {
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
@@ -28,8 +26,6 @@ interface SpaceDetailsProps {
|
|||||||
export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: space, isLoading, refetch } = useSpaceQuery(spaceId);
|
const { data: space, isLoading, refetch } = useSpaceQuery(spaceId);
|
||||||
const hasEnterpriseAccess = useEnterpriseAccess();
|
|
||||||
const showSharingToggle = !readOnly && hasEnterpriseAccess;
|
|
||||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||||
useDisclosure(false);
|
useDisclosure(false);
|
||||||
const [isIconUploading, setIsIconUploading] = useState(false);
|
const [isIconUploading, setIsIconUploading] = useState(false);
|
||||||
@@ -81,6 +77,7 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
|||||||
fallbackName={space.name}
|
fallbackName={space.name}
|
||||||
size={"60px"}
|
size={"60px"}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
|
||||||
type={AvatarIconType.SPACE_ICON}
|
type={AvatarIconType.SPACE_ICON}
|
||||||
onUpload={handleIconUpload}
|
onUpload={handleIconUpload}
|
||||||
onRemove={handleIconRemove}
|
onRemove={handleIconRemove}
|
||||||
@@ -91,13 +88,6 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
|||||||
|
|
||||||
<EditSpaceForm space={space} readOnly={readOnly} />
|
<EditSpaceForm space={space} readOnly={readOnly} />
|
||||||
|
|
||||||
{showSharingToggle && (
|
|
||||||
<>
|
|
||||||
<Divider my="lg" />
|
|
||||||
<SpacePublicSharingToggle space={space} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<>
|
<>
|
||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
|
|||||||
@@ -5,14 +5,6 @@ import {
|
|||||||
} from "@/features/space/permissions/permissions.type.ts";
|
} from "@/features/space/permissions/permissions.type.ts";
|
||||||
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
||||||
|
|
||||||
export interface ISpaceSharingSettings {
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISpaceSettings {
|
|
||||||
sharing?: ISpaceSharingSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISpace {
|
export interface ISpace {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -26,9 +18,6 @@ export interface ISpace {
|
|||||||
memberCount?: number;
|
memberCount?: number;
|
||||||
spaceId?: string;
|
spaceId?: string;
|
||||||
membership?: IMembership;
|
membership?: IMembership;
|
||||||
settings?: ISpaceSettings;
|
|
||||||
// for updates
|
|
||||||
disablePublicSharing?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMembership {
|
interface IMembership {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export async function deleteWorkspaceMember(data: {
|
|||||||
await api.post("/workspace/members/delete", data);
|
await api.post("/workspace/members/delete", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateWorkspace(data: Partial<IWorkspace>) {
|
export async function updateWorkspace(data: Partial<IWorkspace> & { aiSearch?: boolean }) {
|
||||||
const req = await api.post<IWorkspace>("/workspace/update", data);
|
const req = await api.post<IWorkspace>("/workspace/update", data);
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
@@ -66,9 +66,7 @@ export async function createInvitation(data: ICreateInvite) {
|
|||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acceptInvitation(
|
export async function acceptInvitation(data: IAcceptInvite): Promise<{ requiresLogin?: boolean; }> {
|
||||||
data: IAcceptInvite,
|
|
||||||
): Promise<{ requiresLogin?: boolean }> {
|
|
||||||
const req = await api.post("/workspace/invites/accept", data);
|
const req = await api.post("/workspace/invites/accept", data);
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
@@ -110,3 +108,4 @@ export async function getAppVersion(): Promise<IVersion> {
|
|||||||
const req = await api.post("/version");
|
const req = await api.post("/version");
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,23 +22,16 @@ export interface IWorkspace {
|
|||||||
plan?: string;
|
plan?: string;
|
||||||
hasLicenseKey?: boolean;
|
hasLicenseKey?: boolean;
|
||||||
enforceMfa?: boolean;
|
enforceMfa?: boolean;
|
||||||
aiSearch?: boolean;
|
|
||||||
disablePublicSharing?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkspaceSettings {
|
export interface IWorkspaceSettings {
|
||||||
ai?: IWorkspaceAiSettings;
|
ai?: IWorkspaceAiSettings;
|
||||||
sharing?: IWorkspaceSharingSettings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkspaceAiSettings {
|
export interface IWorkspaceAiSettings {
|
||||||
search?: boolean;
|
search?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkspaceSharingSettings {
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICreateInvite {
|
export interface ICreateInvite {
|
||||||
role: string;
|
role: string;
|
||||||
emails: string[];
|
emails: string[];
|
||||||
|
|||||||
+12
-12
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.25.1",
|
"version": "0.25.0-beta.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -33,28 +33,28 @@
|
|||||||
"@ai-sdk/google": "^3.0.9",
|
"@ai-sdk/google": "^3.0.9",
|
||||||
"@ai-sdk/openai": "^3.0.11",
|
"@ai-sdk/openai": "^3.0.11",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.12",
|
"@ai-sdk/openai-compatible": "^2.0.12",
|
||||||
"@aws-sdk/client-s3": "3.982.0",
|
"@aws-sdk/client-s3": "3.701.0",
|
||||||
"@aws-sdk/lib-storage": "3.982.0",
|
"@aws-sdk/lib-storage": "3.701.0",
|
||||||
"@aws-sdk/s3-request-presigner": "3.982.0",
|
"@aws-sdk/s3-request-presigner": "3.701.0",
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/multipart": "^9.4.0",
|
"@fastify/multipart": "^9.3.0",
|
||||||
"@fastify/static": "^9.0.0",
|
"@fastify/static": "^8.3.0",
|
||||||
"@langchain/core": "1.1.18",
|
"@langchain/core": "1.1.13",
|
||||||
"@langchain/textsplitters": "1.0.1",
|
"@langchain/textsplitters": "1.0.1",
|
||||||
"@nestjs-labs/nestjs-ioredis": "^11.0.4",
|
"@nestjs-labs/nestjs-ioredis": "^11.0.4",
|
||||||
"@nestjs/bullmq": "^11.0.4",
|
"@nestjs/bullmq": "^11.0.4",
|
||||||
"@nestjs/common": "^11.1.11",
|
"@nestjs/common": "^11.1.11",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.1.13",
|
"@nestjs/core": "^11.1.11",
|
||||||
"@nestjs/event-emitter": "^3.0.1",
|
"@nestjs/event-emitter": "^3.0.1",
|
||||||
"@nestjs/jwt": "11.0.0",
|
"@nestjs/jwt": "11.0.0",
|
||||||
"@nestjs/mapped-types": "^2.1.0",
|
"@nestjs/mapped-types": "^2.1.0",
|
||||||
"@nestjs/passport": "^11.0.5",
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-fastify": "^11.1.13",
|
"@nestjs/platform-fastify": "^11.1.11",
|
||||||
"@nestjs/platform-socket.io": "^11.1.13",
|
"@nestjs/platform-socket.io": "^11.1.11",
|
||||||
"@nestjs/schedule": "^6.1.0",
|
"@nestjs/schedule": "^6.1.0",
|
||||||
"@nestjs/terminus": "^11.0.0",
|
"@nestjs/terminus": "^11.0.0",
|
||||||
"@nestjs/websockets": "^11.1.13",
|
"@nestjs/websockets": "^11.1.11",
|
||||||
"@node-saml/passport-saml": "^5.1.0",
|
"@node-saml/passport-saml": "^5.1.0",
|
||||||
"@react-email/components": "0.0.28",
|
"@react-email/components": "0.0.28",
|
||||||
"@react-email/render": "1.0.2",
|
"@react-email/render": "1.0.2",
|
||||||
@@ -92,9 +92,9 @@
|
|||||||
"pdfjs-dist": "^5.4.394",
|
"pdfjs-dist": "^5.4.394",
|
||||||
"pg-tsquery": "^8.4.2",
|
"pg-tsquery": "^8.4.2",
|
||||||
"pgvector": "^0.2.1",
|
"pgvector": "^0.2.1",
|
||||||
|
"postgres": "^3.4.8",
|
||||||
"pino-http": "^11.0.0",
|
"pino-http": "^11.0.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"postgres": "^3.4.8",
|
|
||||||
"postmark": "^4.0.5",
|
"postmark": "^4.0.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ export class HistoryListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastHistory = await this.pageHistoryRepo.findPageLastHistory(page.id, {
|
const lastHistory = await this.pageHistoryRepo.findPageLastHistory(page.id);
|
||||||
includeContent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!lastHistory ||
|
!lastHistory ||
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ export class PageController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: scope to workspaces
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/history')
|
@Post('/history')
|
||||||
async getPageHistory(
|
async getPageHistory(
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ export class PageHistoryService {
|
|||||||
constructor(private pageHistoryRepo: PageHistoryRepo) {}
|
constructor(private pageHistoryRepo: PageHistoryRepo) {}
|
||||||
|
|
||||||
async findById(historyId: string): Promise<PageHistory> {
|
async findById(historyId: string): Promise<PageHistory> {
|
||||||
return await this.pageHistoryRepo.findById(historyId, {
|
return await this.pageHistoryRepo.findById(historyId);
|
||||||
includeContent: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findHistoryByPageId(
|
async findHistoryByPageId(
|
||||||
|
|||||||
@@ -64,18 +64,8 @@ export class ShareController {
|
|||||||
throw new BadRequestException();
|
throw new BadRequestException();
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareData = await this.shareService.getSharedPage(dto, workspace.id);
|
|
||||||
|
|
||||||
const sharingAllowed = await this.shareService.isSharingAllowed(
|
|
||||||
workspace.id,
|
|
||||||
shareData.share.spaceId,
|
|
||||||
);
|
|
||||||
if (!sharingAllowed) {
|
|
||||||
throw new NotFoundException('Shared page not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...shareData,
|
...(await this.shareService.getSharedPage(dto, workspace.id)),
|
||||||
hasLicenseKey: hasLicenseOrEE({
|
hasLicenseKey: hasLicenseOrEE({
|
||||||
licenseKey: workspace.licenseKey,
|
licenseKey: workspace.licenseKey,
|
||||||
isCloud: this.environmentService.isCloud(),
|
isCloud: this.environmentService.isCloud(),
|
||||||
@@ -96,14 +86,6 @@ export class ShareController {
|
|||||||
throw new NotFoundException('Share not found');
|
throw new NotFoundException('Share not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharingAllowed = await this.shareService.isSharingAllowed(
|
|
||||||
share.workspaceId,
|
|
||||||
share.spaceId,
|
|
||||||
);
|
|
||||||
if (!sharingAllowed) {
|
|
||||||
throw new NotFoundException('Share not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return share;
|
return share;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,14 +127,6 @@ export class ShareController {
|
|||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharingAllowed = await this.shareService.isSharingAllowed(
|
|
||||||
workspace.id,
|
|
||||||
page.spaceId,
|
|
||||||
);
|
|
||||||
if (!sharingAllowed) {
|
|
||||||
throw new ForbiddenException('Public sharing is disabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.shareService.createShare({
|
return this.shareService.createShare({
|
||||||
page,
|
page,
|
||||||
authUserId: user.id,
|
authUserId: user.id,
|
||||||
@@ -202,21 +176,8 @@ export class ShareController {
|
|||||||
@Body() dto: ShareIdDto,
|
@Body() dto: ShareIdDto,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
const treeData = await this.shareService.getShareTree(
|
|
||||||
dto.shareId,
|
|
||||||
workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const sharingAllowed = await this.shareService.isSharingAllowed(
|
|
||||||
workspace.id,
|
|
||||||
treeData.share.spaceId,
|
|
||||||
);
|
|
||||||
if (!sharingAllowed) {
|
|
||||||
throw new NotFoundException('Share not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...treeData,
|
...(await this.shareService.getShareTree(dto.shareId, workspace.id)),
|
||||||
hasLicenseKey: hasLicenseOrEE({
|
hasLicenseKey: hasLicenseOrEE({
|
||||||
licenseKey: workspace.licenseKey,
|
licenseKey: workspace.licenseKey,
|
||||||
isCloud: this.environmentService.isCloud(),
|
isCloud: this.environmentService.isCloud(),
|
||||||
|
|||||||
@@ -264,31 +264,6 @@ export class ShareService {
|
|||||||
return ancestor;
|
return ancestor;
|
||||||
}
|
}
|
||||||
|
|
||||||
async isSharingAllowed(
|
|
||||||
workspaceId: string,
|
|
||||||
spaceId: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const result = await this.db
|
|
||||||
.selectFrom('workspaces')
|
|
||||||
.innerJoin('spaces', 'spaces.workspaceId', 'workspaces.id')
|
|
||||||
.select([
|
|
||||||
'workspaces.settings as workspaceSettings',
|
|
||||||
'spaces.settings as spaceSettings',
|
|
||||||
])
|
|
||||||
.where('workspaces.id', '=', workspaceId)
|
|
||||||
.where('spaces.id', '=', spaceId)
|
|
||||||
.executeTakeFirst();
|
|
||||||
|
|
||||||
if (!result) return false;
|
|
||||||
|
|
||||||
const workspaceDisabled =
|
|
||||||
(result.workspaceSettings as any)?.sharing?.disabled === true;
|
|
||||||
const spaceDisabled =
|
|
||||||
(result.spaceSettings as any)?.sharing?.disabled === true;
|
|
||||||
|
|
||||||
return !workspaceDisabled && !spaceDisabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updatePublicAttachments(page: Page): Promise<any> {
|
async updatePublicAttachments(page: Page): Promise<any> {
|
||||||
const prosemirrorJson = getProsemirrorContent(page.content);
|
const prosemirrorJson = getProsemirrorContent(page.content);
|
||||||
const attachmentIds = getAttachmentIds(prosemirrorJson);
|
const attachmentIds = getAttachmentIds(prosemirrorJson);
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types';
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
import { CreateSpaceDto } from './create-space.dto';
|
import { CreateSpaceDto } from './create-space.dto';
|
||||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateSpaceDto extends PartialType(CreateSpaceDto) {
|
export class UpdateSpaceDto extends PartialType(CreateSpaceDto) {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
disablePublicSharing: boolean;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
ForbiddenException,
|
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@@ -18,18 +17,12 @@ import { QueueJob, QueueName } from 'src/integrations/queue/constants';
|
|||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
import { InjectQueue } from '@nestjs/bullmq';
|
import { InjectQueue } from '@nestjs/bullmq';
|
||||||
import { CursorPaginationResult } from '@docmost/db/pagination/cursor-pagination';
|
import { CursorPaginationResult } from '@docmost/db/pagination/cursor-pagination';
|
||||||
import { ShareRepo } from '@docmost/db/repos/share/share.repo';
|
|
||||||
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
|
||||||
import { LicenseCheckService } from '../../../integrations/environment/license-check.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceService {
|
export class SpaceService {
|
||||||
constructor(
|
constructor(
|
||||||
private spaceRepo: SpaceRepo,
|
private spaceRepo: SpaceRepo,
|
||||||
private spaceMemberService: SpaceMemberService,
|
private spaceMemberService: SpaceMemberService,
|
||||||
private shareRepo: ShareRepo,
|
|
||||||
private workspaceRepo: WorkspaceRepo,
|
|
||||||
private licenseCheckService: LicenseCheckService,
|
|
||||||
@InjectKysely() private readonly db: KyselyDB,
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
|
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
|
||||||
) {}
|
) {}
|
||||||
@@ -112,31 +105,6 @@ export class SpaceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof updateSpaceDto.disablePublicSharing !== 'undefined') {
|
|
||||||
const workspace = await this.workspaceRepo.findById(workspaceId, {
|
|
||||||
withLicenseKey: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this.licenseCheckService.isValidEELicense(workspace.licenseKey)
|
|
||||||
) {
|
|
||||||
throw new ForbiddenException(
|
|
||||||
'This feature requires a valid enterprise license',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.spaceRepo.updateSharingSettings(
|
|
||||||
updateSpaceDto.spaceId,
|
|
||||||
workspaceId,
|
|
||||||
'disabled',
|
|
||||||
updateSpaceDto.disablePublicSharing,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (updateSpaceDto.disablePublicSharing) {
|
|
||||||
await this.shareRepo.deleteBySpaceId(updateSpaceDto.spaceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.spaceRepo.updateSpace(
|
return await this.spaceRepo.updateSpace(
|
||||||
{
|
{
|
||||||
name: updateSpaceDto.name,
|
name: updateSpaceDto.name,
|
||||||
|
|||||||
@@ -30,8 +30,4 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
generativeAi: boolean;
|
generativeAi: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
disablePublicSharing: boolean;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { LicenseCheckService } from '../../../integrations/environment/license-check.service';
|
|
||||||
import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
|
import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
|
||||||
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
||||||
import { SpaceService } from '../../space/services/space.service';
|
import { SpaceService } from '../../space/services/space.service';
|
||||||
@@ -34,7 +33,6 @@ import { Queue } from 'bullmq';
|
|||||||
import { generateRandomSuffixNumbers } from '../../../common/helpers';
|
import { generateRandomSuffixNumbers } from '../../../common/helpers';
|
||||||
import { isPageEmbeddingsTableExists } from '@docmost/db/helpers/helpers';
|
import { isPageEmbeddingsTableExists } from '@docmost/db/helpers/helpers';
|
||||||
import { CursorPaginationResult } from '@docmost/db/pagination/cursor-pagination';
|
import { CursorPaginationResult } from '@docmost/db/pagination/cursor-pagination';
|
||||||
import { ShareRepo } from '@docmost/db/repos/share/share.repo';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceService {
|
export class WorkspaceService {
|
||||||
@@ -49,8 +47,6 @@ export class WorkspaceService {
|
|||||||
private userRepo: UserRepo,
|
private userRepo: UserRepo,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private domainService: DomainService,
|
private domainService: DomainService,
|
||||||
private licenseCheckService: LicenseCheckService,
|
|
||||||
private shareRepo: ShareRepo,
|
|
||||||
@InjectKysely() private readonly db: KyselyDB,
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
|
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
|
||||||
@InjectQueue(QueueName.BILLING_QUEUE) private billingQueue: Queue,
|
@InjectQueue(QueueName.BILLING_QUEUE) private billingQueue: Queue,
|
||||||
@@ -362,32 +358,6 @@ export class WorkspaceService {
|
|||||||
delete updateWorkspaceDto.generativeAi;
|
delete updateWorkspaceDto.generativeAi;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof updateWorkspaceDto.disablePublicSharing !== 'undefined') {
|
|
||||||
const currentWorkspace = await this.workspaceRepo.findById(workspaceId, {
|
|
||||||
withLicenseKey: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this.licenseCheckService.isValidEELicense(currentWorkspace.licenseKey)
|
|
||||||
) {
|
|
||||||
throw new ForbiddenException(
|
|
||||||
'This feature requires a valid enterprise license',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.workspaceRepo.updateSharingSettings(
|
|
||||||
workspaceId,
|
|
||||||
'disabled',
|
|
||||||
updateWorkspaceDto.disablePublicSharing,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (updateWorkspaceDto.disablePublicSharing) {
|
|
||||||
await this.shareRepo.deleteByWorkspaceId(workspaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete updateWorkspaceDto.disablePublicSharing;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.workspaceRepo.updateWorkspace(updateWorkspaceDto, workspaceId);
|
await this.workspaceRepo.updateWorkspace(updateWorkspaceDto, workspaceId);
|
||||||
|
|
||||||
const workspace = await this.workspaceRepo.findById(workspaceId, {
|
const workspace = await this.workspaceRepo.findById(workspaceId, {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Kysely } from 'kysely';
|
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
|
||||||
await db.schema.alterTable('spaces').addColumn('settings', 'jsonb').execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
|
||||||
await db.schema.alterTable('spaces').dropColumn('settings').execute();
|
|
||||||
}
|
|
||||||
@@ -17,32 +17,15 @@ import { DB } from '@docmost/db/types/db';
|
|||||||
export class PageHistoryRepo {
|
export class PageHistoryRepo {
|
||||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||||
|
|
||||||
private baseFields: Array<keyof PageHistory> = [
|
|
||||||
'id',
|
|
||||||
'pageId',
|
|
||||||
'slugId',
|
|
||||||
'title',
|
|
||||||
'icon',
|
|
||||||
'coverPhoto',
|
|
||||||
'lastUpdatedById',
|
|
||||||
'spaceId',
|
|
||||||
'workspaceId',
|
|
||||||
'createdAt',
|
|
||||||
];
|
|
||||||
|
|
||||||
async findById(
|
async findById(
|
||||||
pageHistoryId: string,
|
pageHistoryId: string,
|
||||||
opts?: {
|
trx?: KyselyTransaction,
|
||||||
includeContent?: boolean;
|
|
||||||
trx?: KyselyTransaction;
|
|
||||||
},
|
|
||||||
): Promise<PageHistory> {
|
): Promise<PageHistory> {
|
||||||
const db = dbOrTx(this.db, opts?.trx);
|
const db = dbOrTx(this.db, trx);
|
||||||
|
|
||||||
return await db
|
return await db
|
||||||
.selectFrom('pageHistory')
|
.selectFrom('pageHistory')
|
||||||
.select(this.baseFields)
|
.selectAll()
|
||||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
|
||||||
.select((eb) => this.withLastUpdatedBy(eb))
|
.select((eb) => this.withLastUpdatedBy(eb))
|
||||||
.where('id', '=', pageHistoryId)
|
.where('id', '=', pageHistoryId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
@@ -80,7 +63,7 @@ export class PageHistoryRepo {
|
|||||||
async findPageHistoryByPageId(pageId: string, pagination: PaginationOptions) {
|
async findPageHistoryByPageId(pageId: string, pagination: PaginationOptions) {
|
||||||
const query = this.db
|
const query = this.db
|
||||||
.selectFrom('pageHistory')
|
.selectFrom('pageHistory')
|
||||||
.select(this.baseFields)
|
.selectAll()
|
||||||
.select((eb) => this.withLastUpdatedBy(eb))
|
.select((eb) => this.withLastUpdatedBy(eb))
|
||||||
.where('pageId', '=', pageId);
|
.where('pageId', '=', pageId);
|
||||||
|
|
||||||
@@ -93,19 +76,12 @@ export class PageHistoryRepo {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findPageLastHistory(
|
async findPageLastHistory(pageId: string, trx?: KyselyTransaction) {
|
||||||
pageId: string,
|
const db = dbOrTx(this.db, trx);
|
||||||
opts?: {
|
|
||||||
includeContent?: boolean;
|
|
||||||
trx?: KyselyTransaction;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const db = dbOrTx(this.db, opts?.trx);
|
|
||||||
|
|
||||||
return await db
|
return await db
|
||||||
.selectFrom('pageHistory')
|
.selectFrom('pageHistory')
|
||||||
.select(this.baseFields)
|
.selectAll()
|
||||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
|
||||||
.where('pageId', '=', pageId)
|
.where('pageId', '=', pageId)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.orderBy('createdAt', 'desc')
|
.orderBy('createdAt', 'desc')
|
||||||
|
|||||||
@@ -136,20 +136,6 @@ export class ShareRepo {
|
|||||||
await query.execute();
|
await query.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteBySpaceId(spaceId: string): Promise<void> {
|
|
||||||
await this.db
|
|
||||||
.deleteFrom('shares')
|
|
||||||
.where('spaceId', '=', spaceId)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteByWorkspaceId(workspaceId: string): Promise<void> {
|
|
||||||
await this.db
|
|
||||||
.deleteFrom('shares')
|
|
||||||
.where('workspaceId', '=', workspaceId)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getShares(userId: string, pagination: PaginationOptions) {
|
async getShares(userId: string, pagination: PaginationOptions) {
|
||||||
const query = this.db
|
const query = this.db
|
||||||
.selectFrom('shares')
|
.selectFrom('shares')
|
||||||
|
|||||||
@@ -89,26 +89,6 @@ export class SpaceRepo {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSharingSettings(
|
|
||||||
spaceId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
prefKey: string,
|
|
||||||
prefValue: string | boolean,
|
|
||||||
) {
|
|
||||||
return this.db
|
|
||||||
.updateTable('spaces')
|
|
||||||
.set({
|
|
||||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
|
||||||
|| jsonb_build_object('sharing', COALESCE(settings->'sharing', '{}'::jsonb)
|
|
||||||
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
})
|
|
||||||
.where('id', '=', spaceId)
|
|
||||||
.where('workspaceId', '=', workspaceId)
|
|
||||||
.returningAll()
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
async insertSpace(
|
async insertSpace(
|
||||||
insertableSpace: InsertableSpace,
|
insertableSpace: InsertableSpace,
|
||||||
trx?: KyselyTransaction,
|
trx?: KyselyTransaction,
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export class WorkspaceRepo {
|
|||||||
.updateTable('workspaces')
|
.updateTable('workspaces')
|
||||||
.set({
|
.set({
|
||||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||||
|| jsonb_build_object('api', COALESCE(settings->'api', '{}'::jsonb)
|
|| jsonb_build_object('api', COALESCE(settings->'api', '{}'::jsonb)
|
||||||
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
@@ -185,25 +185,7 @@ export class WorkspaceRepo {
|
|||||||
.updateTable('workspaces')
|
.updateTable('workspaces')
|
||||||
.set({
|
.set({
|
||||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||||
|| jsonb_build_object('ai', COALESCE(settings->'ai', '{}'::jsonb)
|
|| jsonb_build_object('ai', COALESCE(settings->'ai', '{}'::jsonb)
|
||||||
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
})
|
|
||||||
.where('id', '=', workspaceId)
|
|
||||||
.returning(this.baseFields)
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateSharingSettings(
|
|
||||||
workspaceId: string,
|
|
||||||
prefKey: string,
|
|
||||||
prefValue: string | boolean,
|
|
||||||
) {
|
|
||||||
return this.db
|
|
||||||
.updateTable('workspaces')
|
|
||||||
.set({
|
|
||||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
|
||||||
|| jsonb_build_object('sharing', COALESCE(settings->'sharing', '{}'::jsonb)
|
|
||||||
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
|
|||||||
-1
@@ -273,7 +273,6 @@ export interface Spaces {
|
|||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
logo: string | null;
|
logo: string | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
settings: Json | null;
|
|
||||||
slug: string;
|
slug: string;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
visibility: Generated<string>;
|
visibility: Generated<string>;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
import { validate } from './environment.validation';
|
import { validate } from './environment.validation';
|
||||||
import { envPath } from '../../common/helpers';
|
import { envPath } from '../../common/helpers';
|
||||||
import { DomainService } from './domain.service';
|
import { DomainService } from './domain.service';
|
||||||
import { LicenseCheckService } from './license-check.service';
|
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
@@ -16,7 +15,7 @@ import { LicenseCheckService } from './license-check.service';
|
|||||||
validate,
|
validate,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [EnvironmentService, DomainService, LicenseCheckService],
|
providers: [EnvironmentService, DomainService],
|
||||||
exports: [EnvironmentService, DomainService, LicenseCheckService],
|
exports: [EnvironmentService, DomainService],
|
||||||
})
|
})
|
||||||
export class EnvironmentModule {}
|
export class EnvironmentModule {}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ModuleRef } from '@nestjs/core';
|
|
||||||
import { EnvironmentService } from './environment.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class LicenseCheckService {
|
|
||||||
constructor(
|
|
||||||
private moduleRef: ModuleRef,
|
|
||||||
private environmentService: EnvironmentService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
isValidEELicense(licenseKey: string): boolean {
|
|
||||||
if (this.environmentService.isCloud()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
||||||
const LicenseModule = require('../../ee/licence/license.service');
|
|
||||||
const licenseService = this.moduleRef.get(LicenseModule.LicenseService, {
|
|
||||||
strict: false,
|
|
||||||
});
|
|
||||||
return licenseService.isValidEELicense(licenseKey);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ import { Readable } from 'stream';
|
|||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||||
import { getMimeType } from '../../../common/helpers';
|
import { getMimeType } from '../../../common/helpers';
|
||||||
import { Upload } from '@aws-sdk/lib-storage';
|
import { Upload } from '@aws-sdk/lib-storage';
|
||||||
import { Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
export class S3Driver implements StorageDriver {
|
export class S3Driver implements StorageDriver {
|
||||||
private readonly s3Client: S3Client;
|
private readonly s3Client: S3Client;
|
||||||
@@ -40,7 +39,6 @@ export class S3Driver implements StorageDriver {
|
|||||||
|
|
||||||
await upload.done();
|
await upload.done();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(err);
|
|
||||||
throw new Error(`Failed to upload file: ${(err as Error).message}`);
|
throw new Error(`Failed to upload file: ${(err as Error).message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +73,6 @@ export class S3Driver implements StorageDriver {
|
|||||||
|
|
||||||
await upload.done();
|
await upload.done();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(err);
|
|
||||||
throw new Error(`Failed to upload file: ${(err as Error).message}`);
|
throw new Error(`Failed to upload file: ${(err as Error).message}`);
|
||||||
} finally {
|
} finally {
|
||||||
if (shouldDestroyClient && clientToUse) {
|
if (shouldDestroyClient && clientToUse) {
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.25.1",
|
"version": "0.25.0-beta.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
"@casl/ability": "6.8.0",
|
"@casl/ability": "6.8.0",
|
||||||
"@docmost/editor-ext": "workspace:*",
|
"@docmost/editor-ext": "workspace:*",
|
||||||
"@floating-ui/dom": "^1.7.3",
|
"@floating-ui/dom": "^1.7.3",
|
||||||
"@hocuspocus/provider": "3.4.4",
|
"@hocuspocus/provider": "3.4.3",
|
||||||
"@hocuspocus/server": "3.4.4",
|
"@hocuspocus/server": "3.4.3",
|
||||||
"@hocuspocus/transformer": "3.4.4",
|
"@hocuspocus/transformer": "3.4.3",
|
||||||
"@joplin/turndown": "^4.0.74",
|
"@joplin/turndown": "^4.0.74",
|
||||||
"@joplin/turndown-plugin-gfm": "^1.0.56",
|
"@joplin/turndown-plugin-gfm": "^1.0.56",
|
||||||
"@sindresorhus/slugify": "1.1.0",
|
"@sindresorhus/slugify": "1.1.0",
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# prosemirror-changeset
|
||||||
|
|
||||||
|
This is a helper module that can turn a sequence of document changes
|
||||||
|
into a set of insertions and deletions, for example to display them in
|
||||||
|
a change-tracking interface. Such a set can be built up incrementally,
|
||||||
|
in order to do such change tracking in a halfway performant way during
|
||||||
|
live editing.
|
||||||
|
|
||||||
|
This code is licensed under an [MIT
|
||||||
|
licence](https://github.com/ProseMirror/prosemirror-changeset/blob/master/LICENSE).
|
||||||
|
|
||||||
|
## Programming interface
|
||||||
|
|
||||||
|
Insertions and deletions are represented as ‘spans’—ranges in the
|
||||||
|
document. The deleted spans refer to the original document, whereas
|
||||||
|
the inserted ones point into the current document.
|
||||||
|
|
||||||
|
It is possible to associate arbitrary data values with such spans, for
|
||||||
|
example to track the user that made the change, the timestamp at which
|
||||||
|
it was made, or the step data necessary to invert it again.
|
||||||
|
|
||||||
|
### class Change`<Data = any>`
|
||||||
|
|
||||||
|
A replaced range with metadata associated with it.
|
||||||
|
|
||||||
|
* **`fromA`**`: number`\
|
||||||
|
The start of the range deleted/replaced in the old document.
|
||||||
|
|
||||||
|
* **`toA`**`: number`\
|
||||||
|
The end of the range in the old document.
|
||||||
|
|
||||||
|
* **`fromB`**`: number`\
|
||||||
|
The start of the range inserted in the new document.
|
||||||
|
|
||||||
|
* **`toB`**`: number`\
|
||||||
|
The end of the range in the new document.
|
||||||
|
|
||||||
|
* **`deleted`**`: readonly Span[]`\
|
||||||
|
Data associated with the deleted content. The length of these
|
||||||
|
spans adds up to `this.toA - this.fromA`.
|
||||||
|
|
||||||
|
* **`inserted`**`: readonly Span[]`\
|
||||||
|
Data associated with the inserted content. Length adds up to
|
||||||
|
`this.toB - this.fromB`.
|
||||||
|
|
||||||
|
* `static `**`merge`**`<Data>(x: readonly Change[], y: readonly Change[], combine: fn(dataA: Data, dataB: Data) → Data) → readonly Change[]`\
|
||||||
|
This merges two changesets (the end document of x should be the
|
||||||
|
start document of y) into a single one spanning the start of x to
|
||||||
|
the end of y.
|
||||||
|
|
||||||
|
|
||||||
|
### class Span`<Data = any>`
|
||||||
|
|
||||||
|
Stores metadata for a part of a change.
|
||||||
|
|
||||||
|
* **`length`**`: number`\
|
||||||
|
The length of this span.
|
||||||
|
|
||||||
|
* **`data`**`: Data`\
|
||||||
|
The data associated with this span.
|
||||||
|
|
||||||
|
|
||||||
|
### class ChangeSet`<Data = any>`
|
||||||
|
|
||||||
|
A change set tracks the changes to a document from a given point
|
||||||
|
in the past. It condenses a number of step maps down to a flat
|
||||||
|
sequence of replacements, and simplifies replacments that
|
||||||
|
partially undo themselves by comparing their content.
|
||||||
|
|
||||||
|
* **`changes`**`: readonly Change[]`\
|
||||||
|
Replaced regions.
|
||||||
|
|
||||||
|
* **`addSteps`**`(newDoc: Node, maps: readonly StepMap[], data: Data | readonly Data[]) → ChangeSet`\
|
||||||
|
Computes a new changeset by adding the given step maps and
|
||||||
|
metadata (either as an array, per-map, or as a single value to be
|
||||||
|
associated with all maps) to the current set. Will not mutate the
|
||||||
|
old set.
|
||||||
|
|
||||||
|
Note that due to simplification that happens after each add,
|
||||||
|
incrementally adding steps might create a different final set
|
||||||
|
than adding all those changes at once, since different document
|
||||||
|
tokens might be matched during simplification depending on the
|
||||||
|
boundaries of the current changed ranges.
|
||||||
|
|
||||||
|
* **`startDoc`**`: Node`\
|
||||||
|
The starting document of the change set.
|
||||||
|
|
||||||
|
* **`map`**`(f: fn(range: Span) → Data) → ChangeSet`\
|
||||||
|
Map the span's data values in the given set through a function
|
||||||
|
and construct a new set with the resulting data.
|
||||||
|
|
||||||
|
* **`changedRange`**`(b: ChangeSet, maps?: readonly StepMap[]) → {from: number, to: number}`\
|
||||||
|
Compare two changesets and return the range in which they are
|
||||||
|
changed, if any. If the document changed between the maps, pass
|
||||||
|
the maps for the steps that changed it as second argument, and
|
||||||
|
make sure the method is called on the old set and passed the new
|
||||||
|
set. The returned positions will be in new document coordinates.
|
||||||
|
|
||||||
|
* `static `**`create`**`<Data = any>(doc: Node, combine?: fn(dataA: Data, dataB: Data) → Data = (a, b) => a === b ? a : null as any, tokenEncoder?: TokenEncoder = DefaultEncoder) → ChangeSet`\
|
||||||
|
Create a changeset with the given base object and configuration.
|
||||||
|
|
||||||
|
The `combine` function is used to compare and combine metadata—it
|
||||||
|
should return null when metadata isn't compatible, and a combined
|
||||||
|
version for a merged range when it is.
|
||||||
|
|
||||||
|
When given, a token encoder determines how document tokens are
|
||||||
|
serialized and compared when diffing the content produced by
|
||||||
|
changes. The default is to just compare nodes by name and text
|
||||||
|
by character, ignoring marks and attributes.
|
||||||
|
|
||||||
|
|
||||||
|
* **`simplifyChanges`**`(changes: readonly Change[], doc: Node) → Change[]`\
|
||||||
|
Simplifies a set of changes for presentation. This makes the
|
||||||
|
assumption that having both insertions and deletions within a word
|
||||||
|
is confusing, and, when such changes occur without a word boundary
|
||||||
|
between them, they should be expanded to cover the entire set of
|
||||||
|
words (in the new document) they touch. An exception is made for
|
||||||
|
single-character replacements.
|
||||||
|
|
||||||
|
|
||||||
|
### interface TokenEncoder`<T>`
|
||||||
|
|
||||||
|
A token encoder can be passed when creating a `ChangeSet` in order
|
||||||
|
to influence the way the library runs its diffing algorithm. The
|
||||||
|
encoder determines how document tokens (such as nodes and
|
||||||
|
characters) are encoded and compared.
|
||||||
|
|
||||||
|
Note that both the encoding and the comparison may run a lot, and
|
||||||
|
doing non-trivial work in these functions could impact
|
||||||
|
performance.
|
||||||
|
|
||||||
|
* **`encodeCharacter`**`(char: number, marks: readonly Mark[]) → T`\
|
||||||
|
Encode a given character, with the given marks applied.
|
||||||
|
|
||||||
|
* **`encodeNodeStart`**`(node: Node) → T`\
|
||||||
|
Encode the start of a node or, if this is a leaf node, the
|
||||||
|
entire node.
|
||||||
|
|
||||||
|
* **`encodeNodeEnd`**`(node: Node) → T`\
|
||||||
|
Encode the end token for the given node. It is valid to encode
|
||||||
|
every end token in the same way.
|
||||||
|
|
||||||
|
* **`compareTokens`**`(a: T, b: T) → boolean`\
|
||||||
|
Compare the given tokens. Should return true when they count as
|
||||||
|
equal.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# prosemirror-recreate-transform
|
||||||
|
|
||||||
|
> reduced and modified fork of https://gitlab.com/mpapp-public/prosemirror-recreate-steps
|
||||||
|
|
||||||
|
This is a non-core module of [ProseMirror](http://prosemirror.net).
|
||||||
|
ProseMirror is a well-behaved rich semantic content editor based on
|
||||||
|
contentEditable, with support for collaborative editing and custom
|
||||||
|
document schemas.
|
||||||
|
|
||||||
|
Every change to the document is recorded by ProseMirror as a step.
|
||||||
|
This module allows recreating the steps needed to go from document
|
||||||
|
A to B should these not be available otherwise. Recreating steps
|
||||||
|
can be interesting for example in order to show the changes between
|
||||||
|
two document versions without having access to the original steps.
|
||||||
|
|
||||||
|
Recreating a `Transform` works this way:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { recreateTransform } from "@technik-sde/prosemirror-recreate-transform";
|
||||||
|
|
||||||
|
let tr = recreateTransform(
|
||||||
|
startDoc,
|
||||||
|
endDoc,
|
||||||
|
{
|
||||||
|
complexSteps: true, // Whether step types other than ReplaceStep are allowed.
|
||||||
|
wordDiffs: false, // Whether diffs in text nodes should cover entire words.
|
||||||
|
simplifyDiffs: true // Whether steps should be merged, where possible
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// https://gitlab.com/mpapp-public/prosemirror-recreate-steps - MIT
|
// https://gitlab.com/mpapp-public/prosemirror-recreate-steps
|
||||||
// https://github.com/sueddeutsche/prosemirror-recreate-transform - MIT
|
// https://github.com/sueddeutsche/prosemirror-recreate-transform
|
||||||
export { recreateTransform, RecreateTransform } from "./recreateTransform";
|
export { recreateTransform, RecreateTransform } from "./recreateTransform";
|
||||||
export type { Options } from "./recreateTransform";
|
export type { Options } from "./recreateTransform";
|
||||||
|
|||||||
Generated
+1083
-1094
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user