Compare commits

...

4 Commits

Author SHA1 Message Date
Salihu 4b9da3a001 ui changes 2025-11-12 16:46:09 +01:00
Salihu 43b6bb41a4 neutral nav button 2025-11-12 16:45:35 +01:00
Salihu 8c9174b506 ui changes 2025-11-12 16:45:35 +01:00
Salihu 03d3a11f71 add footer navigation 2025-11-12 16:45:35 +01:00
12 changed files with 285 additions and 3 deletions
+2 -2
View File
@@ -9,14 +9,14 @@ import ActionButton, {
} from "~/components/ActionButton";
import { undraggableOnDesktop } from "~/styles";
type RealProps = {
export type RealProps = {
$fullwidth?: boolean;
$borderOnHover?: boolean;
$neutral?: boolean;
$danger?: boolean;
};
const RealButton = styled(ActionButton)<RealProps>`
export const RealButton = styled(ActionButton)<RealProps>`
display: ${(props) => (props.$fullwidth ? "block" : "inline-block")};
width: ${(props) => (props.$fullwidth ? "100%" : "auto")};
margin: 0;
+26 -1
View File
@@ -6,7 +6,11 @@ import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import Icon from "@shared/components/Icon";
import { randomElement } from "@shared/random";
import { CollectionPermission, TeamPreference } from "@shared/types";
import {
CollectionDisplayPreferences,
CollectionPermission,
TeamPreference,
} from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import { colorPalette } from "@shared/utils/collections";
import { CollectionValidation } from "@shared/validations";
@@ -30,6 +34,7 @@ export interface FormData {
icon: string;
color: string | null;
sharing: boolean;
displayPreferences: CollectionDisplayPreferences | undefined;
permission: CollectionPermission | undefined;
commenting?: boolean | null;
}
@@ -86,6 +91,10 @@ export const CollectionForm = observer(function CollectionForm_({
permission: collection?.permission,
commenting: collection?.commenting ?? true,
color: iconColor,
displayPreferences: {
showFooterNavigation:
collection?.displayPreferences?.showFooterNavigation ?? false,
},
},
});
@@ -216,6 +225,22 @@ export const CollectionForm = observer(function CollectionForm_({
/>
)}
<Controller
control={control}
name="displayPreferences.showFooterNavigation"
render={({ field }) => (
<Switch
id="showFooterNavigation"
label={t("Show Footer Navigation")}
note={t(
"Show footer navigation on documents within this collection."
)}
checked={!!field.value}
onChange={field.onChange}
/>
)}
/>
<Flex justify="flex-end">
<Button
type="submit"
+44
View File
@@ -2,6 +2,8 @@ import invariant from "invariant";
import { action, comparer, computed, observable, runInAction } from "mobx";
import {
CollectionPermission,
CollectionDisplayPreference,
type CollectionDisplayPreferences,
FileOperationFormat,
type NavigationNode,
NavigationNodeType,
@@ -99,6 +101,12 @@ export default class Collection extends ParanoidModel {
@observable
archivedBy?: User;
/**
* Display preferences for the collection.
*/
@observable
displayPreferences: CollectionDisplayPreferences;
@computed
get searchContent(): string {
return this.name;
@@ -231,6 +239,25 @@ export default class Collection extends ParanoidModel {
}
};
get flatDocuments(): Document[] {
if (!this?.sortedDocuments) {
return [];
}
const flatten = (docs: Document[]): Document[] => {
const result: Document[] = [];
for (const doc of docs) {
result.push(doc);
if (doc.children && doc.children.length > 0) {
result.push(...flatten(doc.children as Document[]));
}
}
return result;
};
return flatten(this.sortedDocuments as Document[]);
}
/**
* Updates the document identified by the given id in the collection in memory.
* Does not update the document in the database.
@@ -452,5 +479,22 @@ export default class Collection extends ParanoidModel {
}
}
/**
* Set the value for a specific display preference key.
*
* @param key The DisplayPreference key to retrieve
* @param value The value to set
*/
setPreference(key: CollectionDisplayPreference, value: boolean) {
this.displayPreferences = {
...this.displayPreferences,
[key]: value,
};
void this.save({
displayPreferences: this.displayPreferences,
});
}
private isFetching = false;
}
@@ -34,6 +34,7 @@ import DocumentMeta from "./DocumentMeta";
import DocumentTitle from "./DocumentTitle";
import first from "lodash/first";
import { getLangFor } from "~/utils/language";
import NavigationButtons from "./NavigationButtons";
const extensions = withUIExtensions(withComments(richExtensions));
@@ -255,6 +256,9 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
{...rest}
/>
<div ref={childRef}>{children}</div>
{document.collection?.displayPreferences?.showFooterNavigation && (
<NavigationButtons document={document} />
)}
</Flex>
);
}
@@ -0,0 +1,157 @@
import { s } from "@shared/styles";
import React, { useMemo } from "react";
import { useHistory } from "react-router-dom";
import styled from "styled-components";
import { css } from "styled-components";
import { RealButton, RealProps } from "~/components/Button";
import useStores from "~/hooks/useStores";
import Document from "~/models/Document";
import breakpoint from "styled-components-breakpoint";
import {
faChevronLeft,
faChevronRight,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { observer } from "mobx-react";
const NavigationButtons = ({ document }: { document: Document }) => {
const { ui } = useStores();
const history = useHistory();
const docs = useMemo(() => {
const { collection } = document;
let navDocs: Record<string, Document | null> = {
prevDoc: null,
nextDoc: null,
};
if (collection && collection.flatDocuments) {
const currentIndex = collection.flatDocuments.findIndex(
(doc) => doc.id === document?.id
);
if (currentIndex !== undefined && currentIndex !== -1) {
const nextIdx = currentIndex + 1;
if (nextIdx < collection.flatDocuments.length) {
navDocs.nextDoc = collection.flatDocuments[nextIdx];
}
const prevIdx = currentIndex - 1;
if (prevIdx > -1) {
navDocs.prevDoc = collection.flatDocuments[prevIdx];
}
}
}
return navDocs;
}, [document.collection?.sortedDocuments, document.collection]);
const handleNavigate = (doc: Document | null) => {
if (doc) {
history.push(doc.url);
}
};
return (
<Wrapper
fullwidth={!(docs.prevDoc && docs.nextDoc)}
sidebarOpen={!ui.sidebarIsClosed}
>
{docs.prevDoc && (
<NavButton
$neutral
$fullwidth={!docs.nextDoc}
onClick={() => handleNavigate(docs.prevDoc)}
>
<ButtonContent>
<FontAwesomeIcon icon={faChevronLeft} />
<ButtonText style={{ alignItems: "flex-end" }}>
<span style={{ fontSize: 12, opacity: 0.8 }}>Previous</span>
<span>{docs.prevDoc?.title}</span>
</ButtonText>
</ButtonContent>
</NavButton>
)}
{docs.nextDoc && (
<NavButton
$neutral
$fullwidth={!docs.prevDoc}
onClick={() => handleNavigate(docs.nextDoc)}
>
<ButtonContent>
<ButtonText style={{ alignItems: "flex-start" }}>
<span style={{ fontSize: 12, opacity: 0.8 }}>Next</span>
<span>{docs.nextDoc?.title}</span>
</ButtonText>
<FontAwesomeIcon icon={faChevronRight} />
</ButtonContent>
</NavButton>
)}
</Wrapper>
);
};
const Wrapper = styled.div<
React.HTMLAttributes<HTMLDivElement> & {
fullwidth?: boolean;
sidebarOpen?: boolean;
}
>`
position: sticky;
margin-bottom: 100px;
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
${breakpoint("tablet")`
margin: 20px 0;
`}
${breakpoint("desktop")`
flex-direction: row;
`}
`;
const NavButton = styled(RealButton)`
height: 60px;
padding: 4px 12px;
font-weight: bold;
color: ${s("text")};
border-radius: 2px;
line-height: 18px;
background: none;
width: 100%;
&:hover {
opacity: 1;
background: none;
}
${breakpoint("desktop")`
${(props: RealProps) => css`
width: ${props.$fullwidth ? "100%" : "50%"};
`}
`}
`;
const ButtonContent = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`;
const ButtonText = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
align-content: center;
justify-items: center;
bacground-color: red;
font-size: 16px;
font-weight: bold;
`;
export default observer(NavigationButtons);
@@ -0,0 +1,21 @@
"use strict";
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn("collections", "displayPreferences", {
type: Sequelize.JSONB,
defaultValue: {},
});
await queryInterface.sequelize.query(`
UPDATE collections
SET "displayPreferences" = '{}'::jsonb
WHERE "displayPreferences" IS NULL
`);
},
async down(queryInterface) {
await queryInterface.removeColumn("collections", "displayPreferences");
},
};
+5
View File
@@ -47,6 +47,7 @@ import {
import isUUID from "validator/lib/isUUID";
import type {
CollectionSort,
CollectionDisplayPreferences,
ProsemirrorData,
SourceMetadata,
} from "@shared/types";
@@ -314,6 +315,10 @@ class Collection extends ParanoidModel<
@Column(DataType.JSONB)
sourceMetadata: SourceMetadata | null;
@Default({})
@Column(DataType.JSONB)
displayPreferences: CollectionDisplayPreferences | null;
// getters
/**
+1
View File
@@ -49,6 +49,7 @@ export default async function presentCollection(
updatedAt: collection.updatedAt,
deletedAt: collection.deletedAt,
archivedAt: collection.archivedAt,
displayPreferences: collection.displayPreferences,
archivedBy: undefined,
};
@@ -64,6 +64,7 @@ router.post(
sort,
index,
commenting,
displayPreferences,
} = ctx.input.body;
const { user } = ctx.state.auth;
@@ -82,6 +83,7 @@ router.post(
sort,
index,
commenting,
displayPreferences,
});
if (data) {
@@ -573,6 +575,7 @@ router.post(
sort,
sharing,
commenting,
displayPreferences,
} = ctx.input.body;
const { user } = ctx.state.auth;
@@ -647,6 +650,10 @@ router.post(
collection.commenting = commenting;
}
if (displayPreferences !== undefined) {
collection.displayPreferences = displayPreferences;
}
await collection.saveWithCtx(ctx);
// must reload to update collection membership for correct policy calculation
+7
View File
@@ -2,6 +2,7 @@ import isUndefined from "lodash/isUndefined";
import isUUID from "validator/lib/isUUID";
import { z } from "zod";
import {
CollectionDisplayPreference,
CollectionPermission,
CollectionStatusFilter,
FileOperationFormat,
@@ -46,6 +47,9 @@ export const CollectionsCreateSchema = BaseSchema.extend({
})
.optional(),
commenting: z.boolean().nullish(),
displayPreferences: z
.record(z.nativeEnum(CollectionDisplayPreference), z.boolean())
.optional(),
}),
});
@@ -181,6 +185,9 @@ export const CollectionsUpdateSchema = BaseSchema.extend({
.optional(),
sharing: z.boolean().optional(),
commenting: z.boolean().nullish(),
displayPreferences: z
.record(z.nativeEnum(CollectionDisplayPreference), z.boolean())
.optional(),
}),
});
@@ -188,6 +188,8 @@
"Allow documents within this collection to be shared publicly on the internet.": "Allow documents within this collection to be shared publicly on the internet.",
"Commenting": "Commenting",
"Allow commenting on documents within this collection.": "Allow commenting on documents within this collection.",
"Show Footer Navigation": "Show Footer Navigation",
"Show footer navigation on documents within this collection.": "Show footer navigation on documents within this collection.",
"Saving": "Saving",
"Save": "Save",
"Creating": "Creating",
+9
View File
@@ -246,6 +246,15 @@ export enum UserPreference {
export type UserPreferences = { [key in UserPreference]?: boolean };
export enum CollectionDisplayPreference {
/** Whether the previous and next document buttons at the bottom of the document should be shown */
showFooterNavigation = "showFooterNavigation",
}
export type CollectionDisplayPreferences = {
[key in CollectionDisplayPreference]?: boolean;
};
export type SourceMetadata = {
/** The original source file name. */
fileName?: string;