Compare commits

..

1 Commits

Author SHA1 Message Date
tommoor 514566ee75 chore: Compressed inefficient images automatically 2025-10-15 00:38:04 +00:00
37 changed files with 146 additions and 307 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Close unsigned PRs
uses: actions/github-script@v8
uses: actions/github-script@v6
with:
script: |
const now = new Date();
@@ -40,7 +40,7 @@ jobs:
github.event.pull_request.head.repo.full_name == github.repository)
steps:
- name: Checkout Branch
uses: actions/checkout@v5
uses: actions/checkout@v2
- name: Compress Images
id: calibre
uses: calibreapp/image-actions@main
@@ -48,7 +48,6 @@ jobs:
githubToken: ${{ secrets.GITHUB_TOKEN }}
# For non-Pull Requests, run in compressOnly mode and we'll PR after.
compressOnly: ${{ github.event_name != 'pull_request' }}
minPctChange: "10"
- name: Create Pull Request
# If it's not a Pull Request then commit any changes as a new PR.
if: |
+13 -13
View File
@@ -25,9 +25,9 @@ jobs:
node-version: [20.x, 22.x]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
@@ -38,8 +38,8 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"
@@ -50,8 +50,8 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"
@@ -65,7 +65,7 @@ jobs:
server: ${{ steps.filter.outputs.server }}
app: ${{ steps.filter.outputs.app }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v2
id: filter
with:
@@ -92,8 +92,8 @@ jobs:
matrix:
test-group: [app, shared]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"
@@ -124,8 +124,8 @@ jobs:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"
@@ -141,8 +141,8 @@ jobs:
if: ${{ needs.changes.outputs.app == 'true' && github.repository == 'outline/outline' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+2 -2
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubicloud-standard-8-arm
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -93,7 +93,7 @@ jobs:
runs-on: ubicloud-standard-8
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
+30
View File
@@ -0,0 +1,30 @@
name: Lint
on:
pull_request:
branches: [main]
jobs:
run-linters:
if: startsWith(github.actor, 'codegen-sh')
name: Run linters
runs-on: ubuntu-latest
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- run: yarn lint --fix
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "Applied automatic fixes"
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
- uses: actions/stale@v5
with:
stale-pr-message: "This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days"
stale-issue-message: "This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days"
-1
View File
@@ -22,7 +22,6 @@ export const inviteUser = createAction({
perform: ({ t }) => {
stores.dialogs.openModal({
title: t("Invite to workspace"),
width: "500px",
content: <Invite onSubmit={stores.dialogs.closeAllModals} />,
});
},
+1 -12
View File
@@ -2,7 +2,6 @@ import * as React from "react";
import styled from "styled-components";
import useBoolean from "~/hooks/useBoolean";
import Initials from "./Initials";
import Tooltip from "../Tooltip";
export enum AvatarSize {
Small = 16,
@@ -23,7 +22,6 @@ export interface IAvatar {
avatarUrl: string | null;
color?: string;
initial?: string;
name?: string;
id?: string;
}
@@ -44,8 +42,6 @@ type Props = {
className?: string;
/** Optional style */
style?: React.CSSProperties;
/** Whether to show a tooltip */
showTooltip?: boolean;
};
function Avatar(props: Props) {
@@ -54,13 +50,12 @@ function Avatar(props: Props) {
style,
variant = AvatarVariant.Round,
className,
showTooltip,
...rest
} = props;
const src = props.src || model?.avatarUrl;
const [error, handleError] = useBoolean(false);
const content = (
return (
<Relative
style={style}
$variant={variant}
@@ -78,12 +73,6 @@ function Avatar(props: Props) {
)}
</Relative>
);
return showTooltip ? (
<Tooltip content={props.alt || model?.name || ""}>{content}</Tooltip>
) : (
content
);
}
Avatar.defaultProps = {
-2
View File
@@ -32,8 +32,6 @@ function Dialogs() {
}}
title={modal.title}
style={modal.style}
width={modal.width}
height={modal.height}
>
{modal.content}
</Modal>
+20 -53
View File
@@ -25,7 +25,6 @@ function ExportDialog({ collection, onSubmit }: Props) {
);
const [includeAttachments, setIncludeAttachments] =
React.useState<boolean>(true);
const [includePrivate, setIncludePrivate] = React.useState<boolean>(true);
const user = useCurrentUser();
const { collections } = useStores();
const { t } = useTranslation();
@@ -45,13 +44,6 @@ function ExportDialog({ collection, onSubmit }: Props) {
[]
);
const handleIncludePrivateChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setIncludePrivate(ev.target.checked);
},
[]
);
const handleSubmit = async () => {
if (collection) {
await collection.export(format, includeAttachments);
@@ -67,7 +59,7 @@ function ExportDialog({ collection, onSubmit }: Props) {
},
});
} else {
await collections.export({ format, includeAttachments, includePrivate });
await collections.export(format, includeAttachments);
toast.success(t("Export started"));
}
onSubmit();
@@ -131,62 +123,37 @@ function ExportDialog({ collection, onSubmit }: Props) {
<Text as="p" size="small" weight="bold">
{item.title}
</Text>
<Text size="small" type="secondary">
{item.description}
</Text>
<Text size="small">{item.description}</Text>
</div>
</Option>
))}
</Flex>
<HR />
<Flex gap={12} column>
<Option>
<input
type="checkbox"
name="includeAttachments"
checked={includeAttachments}
onChange={handleIncludeAttachmentsChange}
/>
<div>
<Text as="p" size="small" weight="bold">
{t("Include attachments")}
</Text>
<Text size="small" type="secondary">
{t("Including uploaded images and files in the exported data")}.
</Text>{" "}
</div>
</Option>
<Option>
<input
type="checkbox"
name="includePrivate"
checked={includePrivate}
onChange={handleIncludePrivateChange}
/>
<div>
<Text as="p" size="small" weight="bold">
{t("Include private collections")}
</Text>
</div>
</Option>
</Flex>
<hr />
<Option>
<input
type="checkbox"
name="includeAttachments"
checked={includeAttachments}
onChange={handleIncludeAttachmentsChange}
/>
<div>
<Text as="p" size="small" weight="bold">
{t("Include attachments")}
</Text>
<Text size="small">
{t("Including uploaded images and files in the exported data")}.
</Text>{" "}
</div>
</Option>
</ConfirmationDialog>
);
}
const HR = styled.hr`
margin: 16px 0;
`;
const Option = styled.label`
display: flex;
align-items: start;
align-items: center;
gap: 16px;
input {
margin-top: 4px;
}
p {
margin: 0;
}
-10
View File
@@ -5,7 +5,6 @@ import styled from "styled-components";
import User from "~/models/User";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import { s } from "@shared/styles";
type Props = {
/** The users to display */
@@ -22,8 +21,6 @@ type Props = {
model: User;
}
>;
/** Whether to show tooltips on hover, defaults to true */
showTooltip?: boolean;
};
function Facepile({
@@ -32,7 +29,6 @@ function Facepile({
size = AvatarSize.Large,
limit = 8,
renderAvatar = Avatar,
showTooltip = true,
...rest
}: Props) {
const { t } = useTranslation();
@@ -55,7 +51,6 @@ function Facepile({
<Component
key={model.id}
{...{
showTooltip,
model,
size,
style: {
@@ -106,11 +101,6 @@ const Avatars = styled(Flex)`
align-items: center;
flex-direction: row-reverse;
cursor: var(--pointer);
*:hover {
clip-path: none !important;
box-shadow: 0 0 0 2px ${s("background")};
}
`;
export default observer(Facepile);
+12 -2
View File
@@ -1,5 +1,6 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import { QuestionMarkIcon } from "outline-icons";
import { transparentize } from "polished";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
@@ -341,9 +342,9 @@ function Option({
{option.description && (
<>
&nbsp;
<Text type="tertiary" size="small" ellipsis>
<Description type="tertiary" size="small" ellipsis>
{option.description}
</Text>
</Description>
</>
)}
</OptionContainer>
@@ -359,6 +360,15 @@ const OptionContainer = styled(Flex)`
min-height: 24px;
`;
const Description = styled(Text)`
@media (hover: hover) {
&:hover,
&:focus {
color: ${(props) => transparentize(0.5, props.theme.accentText)};
}
}
`;
const IconWrapper = styled.span`
display: flex;
justify-content: center;
+12 -43
View File
@@ -5,12 +5,10 @@ import {
ComponentProps,
createContext,
forwardRef,
HTMLAttributes,
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
@@ -49,7 +47,6 @@ import {
ReactZoomPanPinchRef,
} from "react-zoom-pan-pinch";
import { transparentize } from "polished";
import { mergeRefs } from "react-merge-refs";
export enum LightboxStatus {
READY_TO_OPEN,
@@ -99,44 +96,16 @@ type ZoomablePannablePinchableProps = {
children: ReactNode;
panningDisabled: boolean;
disabled: boolean;
onClose?: () => void;
};
const ZoomablePannablePinchable = forwardRef<
ReactZoomPanPinchRef,
ZoomablePannablePinchableProps
>(({ children, panningDisabled, disabled, onClose }, ref) => {
>(({ children, panningDisabled, disabled }, ref) => {
const { isPanning, ...panningHandlers } = usePanning();
const wrapperRef = useRef<ReactZoomPanPinchRef>(null);
const scale = wrapperRef.current?.instance.transformState.scale ?? 1;
const wrapperProps = useMemo(
() =>
({
onClick: (event) => {
if (scale > 1) {
return;
}
if (event.defaultPrevented) {
return;
}
if (
["IMG", "INPUT", "BUTTON", "A"].includes(
(event.target as Element).tagName
)
) {
return;
}
onClose?.();
},
}) satisfies HTMLAttributes<HTMLDivElement>,
[onClose, scale]
);
return (
<ZoomPanPinchContext.Provider value={{ isImagePanning: isPanning }}>
<TransformWrapper
ref={mergeRefs([ref, wrapperRef])}
ref={ref}
disabled={disabled}
doubleClick={{ disabled: true }}
minScale={1}
@@ -147,11 +116,7 @@ const ZoomablePannablePinchable = forwardRef<
{...panningHandlers}
>
<TransformComponent
wrapperStyle={{
width: "100%",
height: "100%",
cursor: isPanning ? "grabbing" : scale > 1 ? "grab" : "zoom-out",
}}
wrapperStyle={{ width: "100%", height: "100%" }}
contentStyle={{
width: "100%",
height: "100%",
@@ -159,7 +124,6 @@ const ZoomablePannablePinchable = forwardRef<
justifyContent: "center",
alignItems: "center",
}}
wrapperProps={wrapperProps}
>
{children}
</TransformComponent>
@@ -174,7 +138,10 @@ function usePanning() {
const onPanningStart: ComponentProps<
typeof TransformWrapper
>["onPanningStart"] = (ref) => {
>["onPanningStart"] = (ref, event) => {
if (!(event.target instanceof HTMLImageElement)) {
return;
}
const zoomedIn = ref.state.scale > 1;
if (zoomedIn) {
setPanning(ref.instance.isPanning);
@@ -190,10 +157,13 @@ function usePanning() {
const onPanningStop: ComponentProps<
typeof TransformWrapper
>["onPanningStop"] = (ref, event) => {
if (!(event.target instanceof HTMLImageElement)) {
return;
}
setPanning(ref.instance.isPanning);
if (dragged.current) {
dragged.current = false;
} else if (event.target instanceof HTMLImageElement) {
} else {
const zoomedOut = Math.abs(ref.state.scale - 1) < 0.001;
if (zoomedOut) {
ref.zoomIn();
@@ -788,7 +758,6 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
}
disabled={status.image === ImageStatus.ERROR}
ref={zoomPanPinchRef}
onClose={close}
>
<Image
ref={imgRef}
@@ -1029,7 +998,7 @@ const StyledImg = styled.img<{
object-fit: contain;
cursor: ${(props) =>
props.$panning
? "grabbing"
? "grab"
: props.$zoomedOut
? "zoom-in"
: props.$zoomedIn
+14 -21
View File
@@ -22,8 +22,6 @@ type Props = {
isOpen: boolean;
title?: React.ReactNode;
style?: React.CSSProperties;
width?: number | string;
height?: number | string;
onRequestClose: () => void;
};
@@ -32,8 +30,6 @@ const Modal: React.FC<Props> = ({
isOpen,
title = "Untitled",
style,
width,
height,
onRequestClose,
}: Props) => {
const wasOpen = usePrevious(isOpen);
@@ -61,7 +57,7 @@ const Modal: React.FC<Props> = ({
>
{isMobile ? (
<Mobile>
<MobileContent>
<Content>
<Centered onClick={(ev) => ev.stopPropagation()} column>
{title && (
<Text size="xlarge" weight="bold">
@@ -70,7 +66,7 @@ const Modal: React.FC<Props> = ({
)}
<ErrorBoundary>{children}</ErrorBoundary>
</Centered>
</MobileContent>
</Content>
<Close onClick={onRequestClose}>
<CloseIcon size={32} />
</Close>
@@ -80,7 +76,7 @@ const Modal: React.FC<Props> = ({
</Back>
</Mobile>
) : (
<Wrapper $width={width} $height={height}>
<Small>
<Centered
onClick={(ev) => ev.stopPropagation()}
// maxHeight needed for proper overflow behavior in Safari
@@ -88,9 +84,9 @@ const Modal: React.FC<Props> = ({
column
reverse
>
<DesktopContent style={style} shadow>
<SmallContent style={style} shadow>
<ErrorBoundary component="div">{children}</ErrorBoundary>
</DesktopContent>
</SmallContent>
<Header>
{title && <Text size="large">{title}</Text>}
<NudeButton onClick={onRequestClose}>
@@ -98,7 +94,7 @@ const Modal: React.FC<Props> = ({
</NudeButton>
</Header>
</Centered>
</Wrapper>
</Small>
)}
</StyledContent>
</Dialog.Portal>
@@ -146,7 +142,7 @@ const Mobile = styled.div`
outline: none;
`;
const MobileContent = styled(Scrollable)`
const Content = styled(Scrollable)`
width: 100%;
padding: 8vh 12px;
@@ -155,10 +151,6 @@ const MobileContent = styled(Scrollable)`
`};
`;
const DesktopContent = styled(Scrollable)`
padding: 8px 24px 24px;
`;
const Centered = styled(Flex)`
width: 640px;
max-width: 100%;
@@ -215,17 +207,14 @@ const Header = styled(Flex)`
padding: 24px 24px 12px;
`;
const Wrapper = styled.div<{
$width?: number | string;
$height?: number | string;
}>`
const Small = styled.div`
animation: ${fadeAndScaleIn} 250ms ease;
margin: 25vh auto auto auto;
width: 75vw;
min-width: 350px;
max-width: ${(props) => props.$width || "450px"};
max-height: ${(props) => props.$height || "70vh"};
max-width: 450px;
max-height: 65vh;
z-index: ${depths.modal};
display: flex;
justify-content: center;
@@ -248,4 +237,8 @@ const Wrapper = styled.div<{
}
`;
const SmallContent = styled(Scrollable)`
padding: 8px 24px 24px;
`;
export default observer(Modal);
@@ -9,8 +9,6 @@ import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles";
import Button, { Inner } from "~/components/Button";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import { transparentize } from "polished";
export const SelectItem = forwardRef<
HTMLDivElement,
@@ -116,10 +114,6 @@ const ItemContainer = styled(Flex)`
color: ${s("accentText")};
fill: ${s("accentText")};
}
${Text} {
color: ${(props) => transparentize(0.5, props.theme.accentText)};
}
}
}
+1 -1
View File
@@ -167,7 +167,7 @@ function usePosition({
offset: 0,
visible: true,
blockSelection: false,
maxWidth: "100%",
maxWidth: width,
};
}
}
+1 -1
View File
@@ -179,7 +179,7 @@ export default class Document extends ArchivableModel implements Searchable {
/**
* Parent document that this is a child of, if any.
*/
@Relation(() => Document, { onArchive: "cascade", onDelete: "cascade" })
@Relation(() => Document, { onArchive: "cascade" })
parentDocument?: Document;
@observable
+1 -5
View File
@@ -58,9 +58,7 @@ function Invite({ onSubmit }: Props) {
onSubmit();
if (response.length > 0) {
toast.success(
t("{{ count }} invites sent", { count: response.length })
);
toast.success(t("We sent out your invites!"));
} else {
toast.message(t("Those email addresses are already invited"));
}
@@ -225,8 +223,6 @@ function Invite({ onSubmit }: Props) {
labelHidden={index !== 0}
onKeyDown={handleKeyDown}
onChange={(ev) => handleChange(ev, index)}
autoComplete="off"
data-1p-ignore
value={invite.name}
required={!!invite.email}
flex
+5 -5
View File
@@ -247,9 +247,9 @@ export default class CollectionsStore extends Store<Collection> {
await this.rootStore.documents.fetchRecentlyViewed();
}
export = (options: {
format: FileOperationFormat;
includeAttachments: boolean;
includePrivate: boolean;
}) => client.post("/collections.export_all", options);
export = (format: FileOperationFormat, includeAttachments: boolean) =>
client.post("/collections.export_all", {
format,
includeAttachments,
});
}
+5 -7
View File
@@ -6,8 +6,6 @@ type DialogDefinition = {
content: React.ReactNode;
isOpen: boolean;
style?: React.CSSProperties;
width?: number | string;
height?: number | string;
onClose?: () => void;
};
@@ -50,12 +48,14 @@ export default class DialogsStore {
content,
replace,
style,
width,
height,
onClose,
}: Omit<DialogDefinition, "isOpen"> & {
}: {
id?: string;
title: string;
content: React.ReactNode;
style?: React.CSSProperties;
replace?: boolean;
onClose?: () => void;
}) => {
setTimeout(
action(() => {
@@ -69,8 +69,6 @@ export default class DialogsStore {
title,
content,
style,
width,
height,
isOpen: true,
onClose,
});
+1 -8
View File
@@ -22,7 +22,6 @@ import { Searchable } from "~/models/interfaces/Searchable";
import type { PaginationParams, PartialExcept, Properties } from "~/types";
import { client } from "~/utils/ApiClient";
import { AuthorizationError, NotFoundError } from "~/utils/errors";
import ParanoidModel from "~/models/base/ParanoidModel";
export enum RPCAction {
Info = "info",
@@ -213,13 +212,7 @@ export default abstract class Store<T extends Model> {
}
LifecycleManager.executeHooks(model.constructor, "beforeRemove", model);
if (model instanceof ParanoidModel) {
model.deletedAt = new Date().toISOString();
} else {
this.data.delete(id);
}
this.data.delete(id);
LifecycleManager.executeHooks(model.constructor, "afterRemove", model);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

-3
View File
@@ -15,7 +15,6 @@ type Props = {
user: User;
format?: FileOperationFormat;
includeAttachments?: boolean;
includePrivate?: boolean;
ctx: APIContext;
};
@@ -35,7 +34,6 @@ async function collectionExporter({
user,
format = FileOperationFormat.MarkdownZip,
includeAttachments = true,
includePrivate = true,
ctx,
}: Props) {
const collectionId = collection?.id;
@@ -54,7 +52,6 @@ async function collectionExporter({
collectionId,
options: {
includeAttachments,
includePrivate,
},
userId: user.id,
teamId: user.teamId,
+2 -6
View File
@@ -1,5 +1,4 @@
import uniqBy from "lodash/uniqBy";
import partition from "lodash/partition";
import { UserRole } from "@shared/types";
import InviteEmail from "@server/emails/templates/InviteEmail";
import env from "@server/env";
@@ -25,7 +24,6 @@ export default async function userInviter(
{ invites }: Props
): Promise<{
sent: Invite[];
unsent: Invite[];
users: User[];
}> {
const { user } = ctx.state.auth;
@@ -63,9 +61,8 @@ export default async function userInviter(
const existingEmails = existingUsers.map(
(existingUser) => existingUser.email
);
const [existingInvites, filteredInvites] = partition(
normalizedInvites,
(invite) => existingEmails.includes(invite.email)
const filteredInvites = normalizedInvites.filter(
(invite) => !existingEmails.includes(invite.email)
);
const users = [];
@@ -115,7 +112,6 @@ export default async function userInviter(
return {
sent: filteredInvites,
unsent: existingInvites,
users,
};
}
-4
View File
@@ -215,17 +215,13 @@ export class Mailer {
name: env.SMTP_NAME,
host: env.SMTP_HOST,
port: env.SMTP_PORT,
// If not explicitly configured we default to using TLS in production
secure: env.SMTP_SECURE ?? env.isProduction,
// Allow connections with no authentication if no username is provided
auth: env.SMTP_USERNAME
? {
user: env.SMTP_USERNAME,
pass: env.SMTP_PASSWORD,
}
: undefined,
// Disable STARTTLS entirely when secure is set to false
ignoreTLS: !env.SMTP_SECURE,
tls: env.SMTP_SECURE
? env.SMTP_TLS_CIPHERS
? {
-1
View File
@@ -28,7 +28,6 @@ import Fix from "./decorators/Fix";
export type FileOperationOptions = {
includeAttachments?: boolean;
includePrivate?: boolean;
permission?: CollectionPermission | null;
};
+1 -12
View File
@@ -19,7 +19,6 @@ import fileOperationPresenter from "@server/presenters/fileOperation";
import FileStorage from "@server/storage/files";
import BaseTask, { TaskPriority } from "./BaseTask";
import { Op } from "sequelize";
import { WhereOptions } from "sequelize";
type Props = {
fileOperationId: string;
@@ -42,26 +41,16 @@ export default abstract class ExportTask extends BaseTask<Props> {
User.findByPk(fileOperation.userId, { rejectOnEmpty: true }),
]);
const where: WhereOptions<Collection> = fileOperation.collectionId
const where = fileOperation.collectionId
? {
teamId: user.teamId,
id: fileOperation.collectionId,
permission: fileOperation.options?.includePrivate
? undefined
: {
[Op.ne]: null,
},
}
: {
teamId: user.teamId,
archivedAt: {
[Op.eq]: null,
},
permission: fileOperation.options?.includePrivate
? undefined
: {
[Op.ne]: null,
},
};
const collections = await Collection.scope("withDocumentStructure").findAll(
+1 -2
View File
@@ -533,7 +533,7 @@ router.post(
validate(T.CollectionsExportAllSchema),
transaction(),
async (ctx: APIContext<T.CollectionsExportAllReq>) => {
const { format, includeAttachments, includePrivate } = ctx.input.body;
const { format, includeAttachments } = ctx.input.body;
const { user } = ctx.state.auth;
const { transaction } = ctx.state;
const team = await Team.findByPk(user.teamId, { transaction });
@@ -544,7 +544,6 @@ router.post(
team,
format,
includeAttachments,
includePrivate,
ctx,
});
-1
View File
@@ -154,7 +154,6 @@ export const CollectionsExportAllSchema = BaseSchema.extend({
.nativeEnum(FileOperationFormat)
.default(FileOperationFormat.MarkdownZip),
includeAttachments: z.boolean().default(true),
includePrivate: z.boolean().default(true),
}),
});
-1
View File
@@ -541,7 +541,6 @@ router.post(
ctx.body = {
data: {
sent: response.sent,
unsent: response.unsent,
users: response.users.map((user) =>
presentUser(user, { includeEmail: !!can(user, "readEmail", user) })
),
+3 -9
View File
@@ -16,15 +16,9 @@ if (input.length === 0) {
const root = path.resolve(__dirname, "..", "..");
const opts = {
cwd: root,
stdio: "inherit",
};
try {
execSync(`npm version ${input.join(" ")} --no-git-tag-version`, opts);
} catch (err) {
console.log("Error updating version:", err.message);
exit(1);
}
execSync(`npm version ${input.join(" ")} --no-git-tag-version`, opts);
const package = require(path.resolve(root, "package.json"));
@@ -48,8 +42,8 @@ fs.writeFileSync(path.resolve(root, "LICENSE"), newLicense);
execSync(`git add package.json`, opts);
execSync(`git add LICENSE`, opts);
execSync(`git commit -m "v${newVersion}" --no-verify`, opts);
execSync(`git tag v${newVersion} -m v${newVersion}`, opts);
execSync(`git commit -m "v${newVersion}"`, opts);
execSync(`git tag v${newVersion}`, opts);
execSync(`git push origin v${newVersion}`, opts);
execSync(`git push origin main`, opts);
+16 -27
View File
@@ -8,7 +8,6 @@ import { useAgent as useFilteringAgent } from "request-filtering-agent";
import env from "@server/env";
import Logger from "@server/logging/Logger";
import { capitalize, defaults } from "lodash";
import { InternalError } from "@server/errors";
interface UrlWithTunnel extends URL {
tunnelMethod?: string;
@@ -57,35 +56,25 @@ export default async function fetch(
Logger.silly("http", `Network request to ${url}`, init);
const { allowPrivateIPAddress, ...rest } = init || {};
const response = await nodeFetch(url, {
...rest,
headers: {
"User-Agent": outlineUserAgent,
...rest?.headers,
},
agent: buildAgent(url, init),
});
try {
const response = await nodeFetch(url, {
...rest,
headers: {
"User-Agent": outlineUserAgent,
...rest?.headers,
},
agent: buildAgent(url, init),
if (!response.ok) {
Logger.silly("http", `Network request failed`, {
url,
status: response.status,
statusText: response.statusText,
headers: response.headers.raw(),
});
if (!response.ok) {
Logger.silly("http", `Network request failed`, {
url,
status: response.status,
statusText: response.statusText,
headers: response.headers.raw(),
});
}
return response;
} catch (err) {
if (!env.isCloudHosted && err.message?.startsWith("DNS lookup")) {
throw InternalError(
`${err.message}\n\nTo allow this request, add the IP address to the ALLOWED_PRIVATE_IP_ADDRESSES environment variable.`
);
}
throw err;
}
return response;
}
/**
-31
View File
@@ -1,31 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import Image from "../components/Img";
import { EmbedProps as Props } from ".";
import { useTheme } from "styled-components";
function PlantUmlDiagrams({ matches, ...props }: Props) {
const theme = useTheme();
const mode = theme.isDark ? "dsvg" : "svg";
const title = props.attrs.href.split("/uml/")[1];
const finalUrl = `https://www.plantuml.com/plantuml/${mode}/${title}`;
return (
<Frame
{...props}
src={finalUrl}
icon={
<Image
src="/images/plantuml.png"
alt="PlantUml"
width={16}
height={16}
/>
}
canonicalUrl={props.attrs.href}
border
/>
);
}
export default PlantUmlDiagrams;
-10
View File
@@ -20,7 +20,6 @@ import Spotify from "./Spotify";
import Trello from "./Trello";
import Vimeo from "./Vimeo";
import YouTube from "./YouTube";
import PlantUmlDiagrams from "./PlantUml";
export type EmbedProps = {
isSelected: boolean;
@@ -678,15 +677,6 @@ const embeds: EmbedDescriptor[] = [
icon: <Img src="/images/youtube.png" alt="YouTube" />,
component: YouTube,
}),
new EmbedDescriptor({
title: "Plant UML",
keywords: "plant plantuml uml",
regexMatch: [
/(?:https?:\/\/)?(?:www\.)?editor\.plantuml\.com\/uml\/([a-zA-Z0-9\-_]+)([\&\?].*)?$/i,
],
icon: <Img src="/images/plantuml.png" alt="PlantUml" />,
component: PlantUmlDiagrams,
}),
/* The generic iframe embed should always be the last one */
new EmbedDescriptor({
title: "Embed",
+1 -3
View File
@@ -285,7 +285,6 @@
"You will receive an email when it's complete.": "You will receive an email when it's complete.",
"Include attachments": "Include attachments",
"Including uploaded images and files in the exported data": "Including uploaded images and files in the exported data",
"Include private collections": "Include private collections",
"{{count}} more user": "{{count}} more user",
"{{count}} more user_plural": "{{count}} more users",
"Filter options": "Filter options",
@@ -779,8 +778,7 @@
"Weird, this shouldnt ever be empty": "Weird, this shouldnt ever be empty",
"You havent created any documents yet": "You havent created any documents yet",
"Documents youve recently viewed will be here for easy access": "Documents youve recently viewed will be here for easy access",
"{{ count }} invites sent": "{{ count }} invites sent",
"{{ count }} invites sent_plural": "{{ count }} invites sent",
"We sent out your invites!": "We sent out your invites!",
"Those email addresses are already invited": "Those email addresses are already invited",
"Sorry, you can only send {{MAX_INVITES}} invites at a time": "Sorry, you can only send {{MAX_INVITES}} invites at a time",
"Invited {{roleName}} will receive access to": "Invited {{roleName}} will receive access to",