Skip to content

Commit

Permalink
feat(pathname): add autoNavigate option to hide the "Preview" button …
Browse files Browse the repository at this point in the history
…and automatically navigate as the pathname changes (#63)

* feat(pathname): add autoNavigate option which hides the "Preview" button and automatically navigates as the pathname changes

* chore: add changeset

* fix(pathname): only auto navigate if the current document is active in the Presentation tool

* refactor(pathname): reorder declarations

* fix(pathname): make autoNavigate work properly with i18n

* fix(pathname): always auto-navigate to a newly created document once is has a pathname
  • Loading branch information
marcusforsberg authored Jun 10, 2024
1 parent 4289934 commit 7dd9046
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-ears-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tinloof/sanity-studio": minor
---

Add `autoNavigate` option to hide the "Preview" button and automatically navigate as the pathname changes. Thanks @marcusforsberg!
23 changes: 23 additions & 0 deletions packages/sanity-studio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,29 @@ export default {
};
```

### Automatically navigate on pathname change

By default, the `pathname` field comes with a "Preview" button which is used to navigate to the page within the Presentation iframe when the pathname changes. You can optionally disable this manual button and have the Presentation tool automatically navigate to the new pathname as it changes:

```tsx
import { definePathname } from "@tinloof/sanity-studio";

export default defineType({
type: "document",
name: "modularPage",
fields: [
definePathname({
name: "pathname",
options: {
autoNavigate: true,
},
}),
],
});
```

The Presentation tool will now automatically navigate to the new pathname as the user types, with a 1 second debounce.

## Sections

The `defineSection` field lets you easily define a new section schema. Used in combination with the `SectionsArrayInput` component, it will render a useful section picker in your Sanity documents.
Expand Down
3 changes: 2 additions & 1 deletion packages/sanity-studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
"@tinloof/sanity-web": "workspace:*",
"lodash": "^4.17.21",
"nanoid": "^5.0.7",
"react-rx": "^2.1.3"
"react-rx": "^2.1.3",
"use-debounce": "^10.0.1"
},
"devDependencies": {
"@sanity/pkg-utils": "^6.8.6",
Expand Down
125 changes: 108 additions & 17 deletions packages/sanity-studio/src/components/PathnameFieldComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EditIcon, EyeOpenIcon, FolderIcon, LockIcon } from "@sanity/icons";
import {
PresentationNavigateContextValue,
usePresentationNavigate,
usePresentationParams,
} from "@sanity/presentation";
Expand All @@ -8,6 +9,7 @@ import { getDocumentPath, stringToPathname } from "@tinloof/sanity-web";
import React, { useCallback, useMemo, useRef, useState } from "react";
import { FormFieldValidationStatus, set, unset, useFormValue } from "sanity";
import { styled } from "styled-components";
import { useDebounce, useDebouncedCallback } from "use-debounce";

import { usePathnamePrefix } from "../hooks/usePathnamePrefix";
import {
Expand Down Expand Up @@ -36,6 +38,8 @@ const FolderText = styled(Text)`
}
`;

const pathnameDebounceTime = 1000;

export function PathnameFieldComponent(props: PathnameInputProps): JSX.Element {
const fieldOptions = props.schemaType.options as PathnameOptions | undefined;
const { prefix } = usePathnamePrefix(props);
Expand All @@ -45,6 +49,7 @@ export function PathnameFieldComponent(props: PathnameInputProps): JSX.Element {
defaultLocaleId: undefined,
localizePathname: undefined,
};
const autoNavigate = fieldOptions?.autoNavigate ?? false;
const document = useFormValue([]) as DocumentWithLocale;
const {
inputProps: { onChange, value, readOnly },
Expand All @@ -58,6 +63,28 @@ export function PathnameFieldComponent(props: PathnameInputProps): JSX.Element {
const [folderLocked, setFolderLocked] = useState(!!folder);
const folderCanUnlock = !readOnly && folderOptions.canUnlock;

const navigate = useSafeNavigate();
const preview = useSafePreview();
const debouncedNavigate = useDebouncedCallback((newPreview?: string) => {
if (navigate) {
navigate(newPreview);
}
}, pathnameDebounceTime);

const localizedPathname = getDocumentPath(
{
...document,
locale: i18nOptions.enabled ? document.locale : undefined,
pathname: value?.current,
},
i18nOptions.defaultLocaleId || "",
i18nOptions.localizePathname
);
const [debouncedLocalizedPathname] = useDebounce(
localizedPathname,
pathnameDebounceTime
);

const fullPathInputRef = useRef<HTMLInputElement>(null);
const pathSegmentInputRef = useRef<HTMLInputElement>(null);

Expand All @@ -79,16 +106,50 @@ export function PathnameFieldComponent(props: PathnameInputProps): JSX.Element {
const finalValue = [folder, segment]
.filter((part) => typeof part === "string")
.join("/");
runChange(onChange, finalValue);

runChange({
onChange,
value: finalValue,
document,
i18nOptions,
prevLocalizedPathname: debouncedLocalizedPathname,
preview,
navigate: autoNavigate ? debouncedNavigate : undefined,
});
},
[folder, onChange]
[
folder,
onChange,
document,
i18nOptions,
debouncedLocalizedPathname,
preview,
autoNavigate,
debouncedNavigate,
]
);

const updateFullPath = useCallback(
(e: React.FormEvent<HTMLInputElement>) => {
runChange(onChange, e.currentTarget.value);
runChange({
onChange,
value: e.currentTarget.value,
document,
i18nOptions,
prevLocalizedPathname: debouncedLocalizedPathname,
preview,
navigate: autoNavigate ? debouncedNavigate : undefined,
});
},
[onChange]
[
onChange,
document,
i18nOptions,
debouncedLocalizedPathname,
preview,
autoNavigate,
debouncedNavigate,
]
);

const unlockFolder: React.MouseEventHandler<HTMLButtonElement> = useCallback(
Expand All @@ -107,16 +168,6 @@ export function PathnameFieldComponent(props: PathnameInputProps): JSX.Element {
setFolderLocked(!!folder);
}, [folder, setFolderLocked]);

const localizedPathname = getDocumentPath(
{
...document,
locale: i18nOptions.enabled ? document.locale : undefined,
pathname: value?.current,
},
i18nOptions.defaultLocaleId || "",
i18nOptions.localizePathname
);

const pathInput = useMemo(() => {
if (folderLocked && folder) {
return (
Expand Down Expand Up @@ -166,7 +217,9 @@ export function PathnameFieldComponent(props: PathnameInputProps): JSX.Element {
{...inputValidationProps}
/>
</Box>
<PreviewButton localizedPathname={localizedPathname || ""} />
{!autoNavigate && (
<PreviewButton localizedPathname={localizedPathname || ""} />
)}
</Flex>
);
}
Expand All @@ -185,7 +238,9 @@ export function PathnameFieldComponent(props: PathnameInputProps): JSX.Element {
{...inputValidationProps}
/>
</Box>
<PreviewButton localizedPathname={localizedPathname || ""} />
{!autoNavigate && (
<PreviewButton localizedPathname={localizedPathname || ""} />
)}
</Flex>
);
}, [
Expand All @@ -201,6 +256,7 @@ export function PathnameFieldComponent(props: PathnameInputProps): JSX.Element {
inputValidationProps,
localizedPathname,
folderCanUnlock,
autoNavigate,
]);

return (
Expand Down Expand Up @@ -235,7 +291,23 @@ export function PathnameFieldComponent(props: PathnameInputProps): JSX.Element {
);
}

function runChange(onChange: (patch) => void, value?: string) {
function runChange({
document,
onChange,
value,
i18nOptions,
prevLocalizedPathname,
preview,
navigate,
}: {
document: DocumentWithLocale;
onChange: (patch) => void;
value?: string;
i18nOptions?: PathnameOptions["i18n"];
prevLocalizedPathname?: string;
preview?: string | null;
navigate?: PresentationNavigateContextValue;
}) {
// We use stringToPathname to ensure that the value is a valid pathname.
// We also allow trailing slashes to make it possible to create folders
const finalValue = value
Expand All @@ -250,6 +322,25 @@ function runChange(onChange: (patch) => void, value?: string) {
})
: unset()
);

// Auto-navigate to the updated path in Presentation if enabled
if (navigate) {
const newLocalizedPathname = getDocumentPath(
{
...document,
locale: i18nOptions?.enabled ? document.locale : undefined,
pathname: finalValue,
},
i18nOptions?.defaultLocaleId || "",
i18nOptions?.localizePathname
);

// Auto-navigate if this document is currently being previewed,
// or if it's a brand new document being created.
if (preview === prevLocalizedPathname || !document._createdAt) {
navigate(newLocalizedPathname);
}
}
}

function PreviewButton({ localizedPathname }: { localizedPathname: string }) {
Expand Down
1 change: 1 addition & 0 deletions packages/sanity-studio/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export type PathnameOptions = SlugOptions & {
defaultLocaleId?: string;
localizePathname?: LocalizePathnameFn;
};
autoNavigate?: boolean;
};

export type PathnameParams = Omit<
Expand Down
14 changes: 13 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7dd9046

Please # to comment.