Copy id of block in the UI

I’m trying to build a page with blocks, on which one block is able to link to another (as in an anchor link with scroll into view).

Using the block’s id would be the best solution I guess. But as far as I can tell, there’s no way to copy that Id from the UI, only in the code. Would it be possible to add an option to this context menu for reading/copying the block’s id?

Captura de Tela 2025-01-28 às 09.47.00

Thank you in advance.

Hey @thiago,

Hmm, I think I understand your use case here, but I don’t think the block’s internal ID is the safest way to do this for public use (like in HTML anchors). That a block has an internal ID at all is just a side effect of how they are represented in our internal database, but normally it’s safer to think of blocks as just embedded parts of their parent record. Because the IDs aren’t really part of the block’s content, they could become “detached” or orphaned or if you copy blocks, etc. And if you have a bunch of links to non-human-readable block UUIDs like at4YWxNgRPeYrCz7HTMyZQ and dc5wWYOeRp-S4DVWqhbgQQ, it can become pretty confusing quickly and you might lose track of what points to what.

Instead, can you maybe add another single-text field inside that block, like “HTML ID”, that your editors can manually define? On the frontend you just have to render that as <a name={block.htmlId}/> or <div id={block.htmlId}/>. And in the Dato UI, you can link to it by that ID, like #myfirstblock. This might also be better for SEO and sharing links. Would that work for you?

Thanks for the reply, Roger.

Yeah, I figured it wouldn’t be ideal, but in my case I think it’s preferable, just because there are multiple types of blocks that are candidates for target sections, and I don’t want to keep adding this field to each block (specially since it will be seldom used).

As for the html id, once I know which block is which, I can base the actual html id on some more meaningful field.

I guess since this is not a great solution it’s unreasonable to expect it to be added to that context menu, so I guess I’ll end up getting the Ids from the CDA. It won’t change to frequently, so it’s ok if only engineers can add it…

Thank you!

I see, thank you for explaining, @thiago. Hmm, given all that you said… maybe there is a way to use a plugin to expose the ID and make it easier to copy? Let me poke around the SDK for you and see if I can find anything appropriate. Please give me a day or two on that.

Or you can look for yourself at https://www.datocms.com/docs/plugin-sdk/introduction

Thanks Roger, that’s awesome. I’ve started poking around with the plugin, and was able to create a plugin and see it working.

Any idea of which specific hook I should use to add an option to the context menu of a block?

Also, is there a way of adding things to the clipboard on the plugin code?

That’s what I’m looking for, too… that may be a hook we’re currently missing (and if so it’d be a good feature request on its own), but in the meantime there might be something close enough that we might be able to use (like maybe adding a menu item to the structured text field’s dropdown instead of each block’s; it would have to enumerate all the blocks within that field and let you choose one to copy an ID for). Or maybe there’s a way to put it next to one of the fields within a block… I’ll look and see!

Yes, you should be able to just use navigator.clipboard.writeText(). Our plugin iframe is instantiated with allow='clipboard-write', so it should just work.

Hey @thiago,

I made a simple plugin demonstrating a super basic version of this:

Clicking one of the blocks in the list at the bottom will copy its ID to the clipboard.

You can get the source here: https://github.com/arcataroger/datocms-plugin-block-id-copy/blob/treeview-addon/src/entrypoints/BlockIdCopy.tsx (please use the treeview-addon branch; main is currently being used to debug another issue).

And try it on this demo project (invitation in your email).

For now it’s just a simple field addon. Unfortunately, there doesn’t seem to be a way to override each block’s dropdown menu. There IS a way to override the field’s menu, but that currently has a separate bug (reported to the devs) that prevents sub-menus from displaying correctly.

But at least this gives you the basic functionality of being able to easily see & copy IDs.

Hi, Roger, thanks for your help! I actually had started to type a message with what I already had yesterday, but didn’t click send.

Captura de Tela 2025-01-29 às 15.47.07

Sorry, could have saved you some work!

Great, glad you got that working! That should work fine for a single level of blocks (no nesting). If you need submenus (for blocks within blocks), that’s currently bugged… I’ll let you know if that gets fixed.

Hi, Roger, I still haven’t been able to make the copy to clipboard work.

navigator.clipboard.writeText()

Results in the following error messages on devtools:

dato-customization.vercel.app/:1 Uncaught (in promise) NotAllowedError: Failed to execute 'writeText' on 'Clipboard': Write permission denied.Understand this errorAI
QMVcyMp7RHa8lnRo249nBw:1 ClipboardReadWrite permission has been blocked because of a permissions policy applied to the current document. See https://goo.gl/EuHzyv for more details.

The iframe does have allow='clipboard-write'. I’ve tried deploying the plugin, so that the URL would have https, but the same error remains. The link that chrome directs to also doesn’t work. Do you have any clues?

This is the code:

import { ExecuteFieldDropdownActionCtx, connect } from "datocms-plugin-sdk";
import "datocms-react-ui/styles.css";
import ConfigScreen from "./entrypoints/ConfigScreen";
import { render } from "./utils/render";

connect({
	renderConfigScreen(ctx) {
		return render(<ConfigScreen ctx={ctx} />);
	},
	fieldDropdownActions(field, ctx) {
		const fieldType = field.attributes.field_type
		if (fieldType !== "rich_text") {
			return [];
		}
		const values = ctx.formValues[field.attributes.api_key] as {itemId: string, itemTypeId: string}[]
		return values.map((value, index) => ({
				id: `copyidof-${value.itemId}`,
				label: `Copy ID of item ${index + 1} (${ctx.itemTypes[value.itemTypeId]?.attributes.name ?? "Unknown"})`,
				icon: "clipboard"
			})
		)
  },

  async executeFieldDropdownAction(
    actionId: string,
    ctx: ExecuteFieldDropdownActionCtx,
  ) {
    if (actionId.startsWith("copyidof-")) {
			const id = actionId.split("-")[1]
      ctx.notice(`Id copied: ${id}`);
      window.focus()
      window.navigator.clipboard.writeText(id)
    }
  },
});

Edit: Works on Safari, saw the issue on Chrome

@thiago,

Ah, good catch, sorry =/ There’s a few issues with that approach:

  • The dropdown actions are not inside their own iframe, so the content security policy doesn’t matter :frowning:
  • There’s a focus issue, as you saw
  • Some browsers have an additional restriction on clipboard.writeText() that requires a user interaction (like a click) before you can do that

The workarounds I could think of are:

  1. Using my plugin demo above (shows a nested list below the field) in post #9, which is all inside the iframe and solves the focus/interaction problem by requiring the user to click in the tree itself
  2. You can pop up a modal with a button that does the copy, like:
    Screenshot 000994

The code for #2 would be something like:

async executeFieldDropdownAction(
        actionId: string,
        ctx
    ) {

        if (actionId.startsWith("copyidof-")) {
            const id = actionId.split("-")[1]
            await ctx.openModal({
                id: 'clipboardHackModal',
                parameters: { id: id},
            });
        }
    },
    renderModal(modalId: string, ctx) {
        if (modalId === 'clipboardHackModal') {
            return render(<ClipboardHackModal ctx={ctx} />);
        }
    },
)

Where <ClipboardHackModal/> is:

import {Button, Canvas} from 'datocms-react-ui';
import {RenderModalCtx} from "datocms-plugin-sdk";
import {useEffect} from "react";

type PropTypes = {
    ctx: RenderModalCtx;
};

export const ClipboardHackModal = ({ctx}: PropTypes) => {
    const {id} = ctx.parameters as { id: string };

    const handleCopy = () => {
        navigator.clipboard.writeText(id);
        ctx.notice(`Copied ${id} to clipboard`);
        ctx.resolve(null) // Close the modal
    }
    
    return <Canvas ctx={ctx}>
        <Button buttonSize={"xl"} style={{display: 'block', margin: '0 auto'}} onClick={handleCopy}>
            Copy block ID {id}
        </Button>
    </Canvas>
}

Sorry for the hassle :frowning: