Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions docs/src/content/docs/extending/the-plugin-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,93 @@ does. A copy, or a change to several rows, keeps the modal open with
`busy` until the write ends. Taking all of these from the SDK keeps
your plugin on the admin's own copy.

A modal that stays open during its write, such as a `ConfirmBody`
you run yourself or a busy `FormBody`, closes through
`useCloseWhileOpen(closeModal)`. Every dialog on a row gets the same
`closeModal`, so a write that ends after the reader dismissed your
modal and opened another one would shut the new one. The close
`useCloseWhileOpen` returns does nothing once your modal left the
screen. A dialog your screen holds open on one row loses the button
that opened it when the list draws that row again, and the focus
falls to the page. `useFocusBack` finds the row again and puts the
focus back on the `Trigger` that `focusedTrigger()` noted when the
dialog opened. Mark the title cell of each row with the id DataViews
keys the row by, as `data-item-id`. `focusInDialog()` tells whether a
dialog holds the focus, for a write you finish yourself outside a run.

A rename, or a form that closes before its write, is gone by the time
a write fails. Keep what the reader typed, so the next open of the
modal starts from it and the reader can try once more.
`useUnsavedText` from the SDK holds that text, by item id, for as long
as the screen stays. Keep the store in the hook that builds your
actions, never in the modal, which goes the moment the reader saves.
Pass the item's kept text as the first value of the field. Clear the
item's text before each write, so a save that works leaves nothing
behind. `markLatest` marks the write as the latest one sent to the
item and returns a check. Keep the failed text only while that check
holds, so an earlier save that fails late never overwrites what a
later save sent:

```tsx
import { RenameBody, markLatest, useToaster, useUnsavedText } from '@gophenberg/frontend-sdk'
import type { Action } from '@gophenberg/frontend-sdk/dataviews'
import { useMemo } from 'react'

function useRenameAction(): Action<Note> {
const toaster = useToaster()
const renames = useUnsavedText()
return useMemo<Action<Note>>(
() => ({
id: 'rename',
label: 'Rename…',
modalHeader: 'Rename',
modalFocusOnMount: 'firstContentElement',
RenderModal: ({ items: [note], closeModal }) => (
<RenameBody
name={renames.get(note.id) ?? note.title}
fieldLabel="Name"
submitLabel="Save"
cancelLabel="Cancel"
onSubmit={(title) => {
closeModal?.()
renames.delete(note.id)
const stillLatest = markLatest(renames, note.id)
renameNote(note.id, title)
.then(() => toaster.show('Name updated.'))
.catch((failure) => {
if (stillLatest()) {
renames.set(note.id, title)
}
toaster.show(failure.message)
})
}}
onCancel={closeModal}
/>
),
}),
[toaster, renames],
)
}
```

The store holds any value, so a form with several fields keeps them
all, as `useUnsavedText<NoteFields>()`. Type a store you pass on with
`UnsavedText`, from the SDK too.

`ListEmpty` is what your list shows when it has no rows. It titles
the empty list one level under your page's title, as the admin's own
lists do. Pass it to DataViews as `empty`, with an icon, a title and
an optional hint:

```tsx
import { ListEmpty, postIcon } from '@gophenberg/frontend-sdk'
import { DataViews } from '@gophenberg/frontend-sdk/dataviews'

<DataViews {...props} empty={<ListEmpty icon={postIcon} title="No notes yet." hint="Add one with Add Note." />} />
```

Leave the hint out when a search or a filter emptied the list.

## Versions

Each version below says what it adds and what it changes. When a
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"dependencies": {
"@gophenberg/frontend-sdk": "workspace:*",
"@gopherium/godmin": "0.26.0",
"@gopherium/godmin": "0.27.0",
"@gopherium/gottext": "0.7.0",
"@gopherium/react-auth": "0.15.0",
"@tanstack/react-query": "^5.102.8",
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/content/NameModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

import { InputControl } from '@gophenberg/frontend-sdk'
import type { RenderModalProps } from '@gophenberg/frontend-sdk/dataviews'
import { FormBody, useToaster } from '@gopherium/godmin'
import { FormBody, markLatest, useCloseWhileOpen, useToaster } from '@gopherium/godmin'
import type { UnsavedText } from '@gopherium/godmin'
import { useQueryClient } from '@tanstack/react-query'
import type { QueryClient } from '@tanstack/react-query'
import { __, _x, sprintf } from '@wordpress/i18n'
import { useState } from 'react'

import { RenameModal } from '../lists/RenameModal'
import { useCloseWhileOpen } from '../lists/closeWhileOpen'
import { markLatest } from '../lists/unsavedText'
import type { UnsavedText } from '../lists/unsavedText'
import { useRefresh } from './actions'
import { createPost, fetchPost, renamePost } from './api'
import type { Post } from './api'
Expand Down
9 changes: 3 additions & 6 deletions frontend/src/content/TypeModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

import { InputControl, Text } from '@gophenberg/frontend-sdk'
import type { RenderModalProps } from '@gophenberg/frontend-sdk/dataviews'
import { FormBody, RunConfirm, useToaster } from '@gopherium/godmin'
import { FormBody, RunConfirm, markLatest, useCloseWhileOpen, useToaster } from '@gopherium/godmin'
import type { UnsavedText } from '@gopherium/godmin'
import { FormDialog } from '@gopherium/godmin/dialogs'
import { useQueryClient } from '@tanstack/react-query'
import { __, _x, sprintf } from '@wordpress/i18n'
import { useState } from 'react'

import { useCloseWhileOpen } from '../lists/closeWhileOpen'
import { markLatest } from '../lists/unsavedText'
import type { UnsavedText } from '../lists/unsavedText'
import { typesQueryKey } from './nav'
import { pluralOf } from './typeText'
import { createType, slugifyKey } from './types'
Expand Down Expand Up @@ -156,7 +154,6 @@ export function MakeDefaultModal({
}: RenderModalProps<ContentType> & { runOnList: RunOnList }) {
const client = useQueryClient()
const toaster = useToaster()
const close = useCloseWhileOpen(closeModal)
const [holder] = useState(() => client.getQueryData<ContentType[]>(typesQueryKey)?.find((held) => held.isDefault))
const [type] = items
const handOver = () =>
Expand All @@ -170,7 +167,7 @@ export function MakeDefaultModal({
return (
<RunConfirm
items={items}
closeModal={close}
closeModal={closeModal}
confirmLabel={__('Make default', 'gophenberg')}
cancelLabel={__('Cancel', 'gophenberg')}
run={() => runOnList(items, handOver)}
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/content/TypesScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type { UseQueryResult } from '@tanstack/react-query'
import { __, sprintf } from '@wordpress/i18n'
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'

import { useFocusOnList } from '../lists/focusOnList'
import type { ListSettings } from '../settings/api'
import { useListSettings } from '../settings/useListSettings'
import { typesMenuItem, typesQueryKey } from './nav'
Expand Down Expand Up @@ -142,8 +141,7 @@ export function TypesScreen() {
const fields = useTypeFields(types.data)
const { list, view, settings } = useTypesView(fields)
const { run, region } = useListRun('content-types', list.onChangeSelection)
const onList = useFocusOnList(run, region)
const actions = useTypeActions(onList, usePageKeys(view, fields))
const actions = useTypeActions(run, usePageKeys(view, fields))
return (
<Page
title={__('Content Types', 'gophenberg')}
Expand Down
13 changes: 4 additions & 9 deletions frontend/src/content/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@

import { backupIcon, sessionMayChange, trashIcon } from '@gophenberg/frontend-sdk'
import type { Action, RenderModalProps } from '@gophenberg/frontend-sdk/dataviews'
import { RunConfirm, useBulkRun, useToaster } from '@gopherium/godmin'
import type { BulkRun, ListRun } from '@gopherium/godmin'
import { RunConfirm, useBulkRun, useToaster, useUnsavedText } from '@gopherium/godmin'
import type { BulkRun, ListRun, UnsavedText } from '@gopherium/godmin'
import { useSession } from '@gopherium/react-auth'
import { __, _x } from '@wordpress/i18n'
import { useQueryClient } from '@tanstack/react-query'
import { useCallback, useMemo } from 'react'

import { useCloseWhileOpen } from '../lists/closeWhileOpen'
import { deleteQuestion, deleteWords } from '../lists/itemWords'
import type { NameCut } from '../lists/itemWords'
import { useUnsavedText } from '../lists/unsavedText'
import type { UnsavedText } from '../lists/unsavedText'
import { postName, restoreWords, trashQuestion, trashWords } from './actionWords'
import { deletePost, restorePost, trashPost } from './api'
import type { Post } from './api'
Expand Down Expand Up @@ -61,11 +58,10 @@ interface PostConfirmProps extends RenderModalProps<Post> {
* @returns The confirm.
*/
function TrashConfirm({ items, closeModal, run, name }: PostConfirmProps) {
const close = useCloseWhileOpen(closeModal)
return (
<RunConfirm
items={items}
closeModal={close}
closeModal={closeModal}
confirmLabel={__('Move to trash', 'gophenberg')}
cancelLabel={__('Cancel', 'gophenberg')}
run={(picked) => run(picked, (post) => trashPost(post.id), trashWords(name))}
Expand All @@ -81,11 +77,10 @@ function TrashConfirm({ items, closeModal, run, name }: PostConfirmProps) {
* @returns The confirm.
*/
function DeleteConfirm({ items, closeModal, run, name }: PostConfirmProps) {
const close = useCloseWhileOpen(closeModal)
return (
<RunConfirm
items={items}
closeModal={close}
closeModal={closeModal}
confirmLabel={__('Delete permanently', 'gophenberg')}
cancelLabel={__('Cancel', 'gophenberg')}
run={(picked) => run(picked, (post) => deletePost(post.id), deleteWords(name, postName))}
Expand Down
10 changes: 3 additions & 7 deletions frontend/src/content/typeActions.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
// SPDX-License-Identifier: Apache-2.0

import type { Action, RenderModalProps } from '@gophenberg/frontend-sdk/dataviews'
import { RunConfirm, useBulkRun, useToaster } from '@gopherium/godmin'
import type { BulkRun, ListRun } from '@gopherium/godmin'
import { RunConfirm, useBulkRun, useToaster, useUnsavedText } from '@gopherium/godmin'
import type { BulkRun, ListRun, UnsavedText } from '@gopherium/godmin'
import { useQueryClient } from '@tanstack/react-query'
import type { QueryClient } from '@tanstack/react-query'
import { __ } from '@wordpress/i18n'
import { useCallback, useMemo } from 'react'

import { useCloseWhileOpen } from '../lists/closeWhileOpen'
import type { NameCut } from '../lists/itemWords'
import { useUnsavedText } from '../lists/unsavedText'
import type { UnsavedText } from '../lists/unsavedText'
import { originOf } from './typeFields'
import { AddressModal, DescribeModal, MakeDefaultModal } from './TypeModals'
import type { RunOnList } from './TypeModals'
Expand Down Expand Up @@ -146,11 +143,10 @@ function switchActions(tools: ActionTools): Action<ContentType>[] {
* @returns The confirm.
*/
function DeleteTypesModal({ items, closeModal, tools }: RenderModalProps<ContentType> & { tools: ActionTools }) {
const close = useCloseWhileOpen(closeModal)
return (
<RunConfirm
items={items}
closeModal={close}
closeModal={closeModal}
confirmLabel={__('Delete', 'gophenberg')}
cancelLabel={__('Cancel', 'gophenberg')}
run={(types) => tools.removals(types, (type) => deleteInTurn(tools.client, type.key), removalWords(tools.name))}
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/lists/RenameModal.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
// SPDX-License-Identifier: Apache-2.0

import type { RenderModalProps } from '@gophenberg/frontend-sdk/dataviews'
import { RenameBody, useToaster } from '@gopherium/godmin'
import { RenameBody, markLatest, useToaster } from '@gopherium/godmin'
import type { UnsavedText } from '@gopherium/godmin'
import { __ } from '@wordpress/i18n'

import { markLatest } from './unsavedText'
import type { UnsavedText } from './unsavedText'

/** What the send of a new name for one item needs. */
export interface RenameSend<Item> {
/** Returns the id an item is known by. */
Expand Down
23 changes: 0 additions & 23 deletions frontend/src/lists/closeWhileOpen.ts

This file was deleted.

47 changes: 0 additions & 47 deletions frontend/src/lists/focusOnList.ts

This file was deleted.

32 changes: 0 additions & 32 deletions frontend/src/lists/unsavedText.ts

This file was deleted.

Loading
Loading