mirror of
https://github.com/outline/outline.git
synced 2026-06-14 03:45:00 +03:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 514566ee75 |
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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} />,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -32,8 +32,6 @@ function Dialogs() {
|
||||
}}
|
||||
title={modal.title}
|
||||
style={modal.style}
|
||||
width={modal.width}
|
||||
height={modal.height}
|
||||
>
|
||||
{modal.content}
|
||||
</Modal>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
<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
@@ -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
@@ -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)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ function usePosition({
|
||||
offset: 0,
|
||||
visible: true,
|
||||
blockSelection: false,
|
||||
maxWidth: "100%",
|
||||
maxWidth: width,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 |
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
? {
|
||||
|
||||
@@ -28,7 +28,6 @@ import Fix from "./decorators/Fix";
|
||||
|
||||
export type FileOperationOptions = {
|
||||
includeAttachments?: boolean;
|
||||
includePrivate?: boolean;
|
||||
permission?: CollectionPermission | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -154,7 +154,6 @@ export const CollectionsExportAllSchema = BaseSchema.extend({
|
||||
.nativeEnum(FileOperationFormat)
|
||||
.default(FileOperationFormat.MarkdownZip),
|
||||
includeAttachments: z.boolean().default(true),
|
||||
includePrivate: z.boolean().default(true),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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) })
|
||||
),
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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 shouldn’t ever be empty": "Weird, this shouldn’t ever be empty",
|
||||
"You haven’t created any documents yet": "You haven’t created any documents yet",
|
||||
"Documents you’ve recently viewed will be here for easy access": "Documents you’ve 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",
|
||||
|
||||
Reference in New Issue
Block a user