Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Placeholders for Widget Defaults and add UUID Placeholder #1975

Open
Undistraction opened this issue Dec 27, 2018 · 21 comments · May be fixed by #6675
Open

Implement Placeholders for Widget Defaults and add UUID Placeholder #1975

Undistraction opened this issue Dec 27, 2018 · 21 comments · May be fixed by #6675
Labels
area: configuration area: ui/editor extensions/widgets/uuid pinned type: feature code contributing to the implementation of a feature and/or user facing functionality

Comments

@Undistraction
Copy link
Contributor

Undistraction commented Dec 27, 2018

Is your feature request related to a problem? Please describe.
This is an issue to flesh out the discussion in #1407. It aims to describe how the discussed solution would be implemented. The goals are:

  1. Generalise the use of placeholders so that they can be used in places other than in generating the slug field.
  2. Add a uuid placeholder that is replaced by a UUID.
  3. Enable the use of placeholders within widgets' default field.

Proposed Solution

  1. Extract token replacement / placeholders to separate until that allows a configurable map of tokens to be supplied.
  • Extract functionality in slugformatter to a formatter utility to make it generic. Allow a map of tokens => functions to be passed in to customise the behaviour. This way the supported tokens can be varied based on where the formatter is used.
  • Configure a formatter to replicate current slug formatter functionality.
    Note: At this point, functionality should be identical to present.
  1. Add uuid field to the placeholder tokens supported by the slug formatter.
  • Create a function for generating a uuid that wraps [uuid lib]https://www.npmjs.com/package/uuid), using uuidv1().
  • This will allow a uuid to be used inside slugs which I think is worthwhile.
  1. Add formatting to widget's default value resolution.
  • Create a new Formatter for use in processing a widget's default field.
  • Currently there is a check for a default value. If a default value exists it should be run through the formatter and the returned value set as the defaultValue.

There are some further questions, but I don't think they necessarily need to be resolved now and could be added later to keep this feature small and focussed:

  • Could user-defined placeholder functions be supported?
  • Should a UUID field be added by default to all collection items?
  • Could this be opt-in and enabled by a setting on the collection?
  • Should there be a mechanism to add a UUID to existing collection items if one doesn't exist?
  • Should relations default to using a uuid prop as the value for valueField?

Note: I have a little time over the next week and should be able to get this done if the above is acceptable.

Describe alternatives you've considered
There is the alternative described in #1407 which involves creating a widget to dynamically supply a UID. However I think a more low-level solution is preferable. Relations should not be being declared based on properties that can be edited by a user. This solution allows a stable field to be created at creation time and set as a hidden widget to prevent user-editing.

@erquhart
Copy link
Contributor

@Undistraction what do you think about this as a potential solution: #1409 (comment)

@dopry
Copy link
Contributor

dopry commented Jun 13, 2019

Any progress on this feature? It's really needed for relations. CMS use cases like predefined taxonomies, separate author bios to be linked from posts, series collections with previous/last all need a relationship identifier that can survive editing the relationship target.

@erquhart
Copy link
Contributor

Hey @dopry - totally agree on priority, but no one has dug into it yet.

@d4rekanguok
Copy link
Contributor

d4rekanguok commented Aug 15, 2019

Tom mentioned this issue in gitter chat & I'd like to share my custom id widget here for those who needs to solve this issue right now: https://github.com/d4rekanguok/netlify-cms-widgets

Please note that this is an implementation of an alternative that @Undistraction has already mentioned above:

There is the alternative described in #1407 which involves creating a widget to dynamically supply a UID. However I think a more low-level solution is preferable. Relations should not be being declared based on properties that can be edited by a user. This solution allows a stable field to be created at creation time and set as a hidden widget to prevent user-editing.

A bit lack of doc (sorry), but the usage for the id widget is fairly simple. Here's an example:

In cms.js

// Register the widget
import cms from 'netlify-cms-app'
import { Widget as IdWidget } from '@ncwidgets/id'

cms.registerWidget(IdWidget)
cms.init()

In config.yml

collections:
  - label: "Posts"
    name: "posts"
    folder: "_posts"
    create: true
    fields:
      - label: ID
        name: id
        widget: ncw-id
        prefix: post        # will generate post-124hfkjas
        timestamp: true     # will generate 1245119112-post-124hfkjas
        hint: This widget generate an unique read-only id

You can view a demo at https://custom-widgets.netlify.com/

Feedback & contribution is much, much appreciated.

@erquhart
Copy link
Contributor

Thanks so much for sharing @d4rekanguok!

@stale
Copy link

stale bot commented Oct 29, 2019

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Oct 29, 2019
@Undistraction
Copy link
Contributor Author

Not stale

@stale stale bot removed the wontfix label Oct 29, 2019
@JannikWempe
Copy link

This should really be integrated in the core netlify cms. I had a lot of headaches with relations...

@madsem
Copy link

madsem commented Feb 9, 2022

Needed this too, after playing around for a while with the suggestions from this issue and some others, I made this widget and just added some style:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import uuidv4 from 'uuid/v4';

CMS.registerWidget(
  'uuid',
  class extends React.Component {
    static propTypes = {
      onChange: PropTypes.func.isRequired,
      forID: PropTypes.string,
      value: PropTypes.node,
      classNameWrapper: PropTypes.string.isRequired
    };

    static defaultProps = {
      value: ''
    };

    componentDidMount() {
      const { value, onChange } = this.props;

      if (!value) {
        onChange(uuidv4());
      }
    }

    render() {
      const styles = {
        element: {display:"none"},
      };

      const { value, classNameWrapper, forID } = this.props;

      return (
        <span id={forID} className={classNameWrapper} style={styles.element}>
          {value}
        </span>
      );
    }
  }
);

To also hide the label in the UI, I added some css

<style>
label[for*="uuid-field-"] {
      display:none;
    }
</style>

Would be pretty nice though if we had this in the core.

@dopry
Copy link
Contributor

dopry commented Mar 11, 2022

nanoid, https://github.com/ai/nanoid, may be a good optimization on uuid.

@lockevn
Copy link

lockevn commented Aug 21, 2022

I also wish this should be in the core as well.

Thanks to @dopry and @madsem, here is my implementation

to simply put into admin/index.html to use.

// https://github.com/ai/nanoid
function nanoid(t = 21) {
    return crypto.getRandomValues(new Uint8Array(t)).reduce((t, e) => (t += (e &= 63) < 36 ? e.toString(36) : e < 62 ? (e - 26).toString(36).toUpperCase() : e > 62 ? '-' : '_'), '')
}

const UuidWidget = createClass({
    componentDidMount() {
        const { value, onChange } = this.props
        if (!value) {
        onChange(nanoid())
        }
    },

    render() {
        const { value, classNameWrapper, forID } = this.props
        return h(
            'span',
            {
                id: forID,
                style: { fontFamily: 'monospace', marginLeft: '1rem' },
                //className: classNameWrapper
            },
            value
        )
    },
})
CMS.registerWidget('uuid', UuidWidget)

fields in config.yml

- { label: 'UUID', name: 'uuid', widget: uuid }

Result

image

@pensivedog
Copy link

Thank you, @lockevn, I like this solution very much, as I'm using the CDN version of Netlify CMS and this helps keep my implementation super simple.

Just one question: when I add this, each item in a collection needs to be saved via the Netlify CMS GUI in order for the UUID to be saved in the markdown file. This is a problem as I have many hundreds of files that need UUIDs. Is there a way to bulk save after adding this?

@dopry
Copy link
Contributor

dopry commented Nov 28, 2022

@pensivedog It sounds like what you're generally asking about is some form of schema migration. NetlifyCMS doesn't have any schema migration that I am aware of. In the interim, you would need to make a a script yourself to write the id to each file.

@liufuyang
Copy link

Any update on this, would be nice if this could be in core? :) Seems should be easy to add a simple id-ish widget and perhaps support both UUID and nanoid via some configuration.

@dopry
Copy link
Contributor

dopry commented Dec 12, 2022

@liufuyang feel free to submit a PR.

liufuyang added a commit to liufuyang/netlify-cms that referenced this issue Jan 30, 2023
Add a uuid widget which supports options:
* read_only (bool)
* prefix (string)
* use_b32_encoding (bool)

close: decaporg#1975
liufuyang added a commit to liufuyang/netlify-cms that referenced this issue Jan 30, 2023
Add a uuid widget which supports options:
* read_only (bool)
* prefix (string)
* use_b32_encoding (bool)

close: decaporg#1975
@liufuyang liufuyang linked a pull request Jan 30, 2023 that will close this issue
4 tasks
@liufuyang
Copy link

Hey @dopry, so I tried to port the String widget and made a simple UUID widget plus a few options that we really need in our place. Do you think this could be something nice to have here as a default UUID component? Thank you #6675

@dopry
Copy link
Contributor

dopry commented Feb 2, 2023

I'm not a mantainer of netlify-cms. I would recommend that you fix your PR so that all the checks are passing, then a maintainer will be more likely to review and merge your code.

@talves
Copy link
Collaborator

talves commented Feb 2, 2023

Before you spend any more time making a PR into this repository, I'd evaluate the "What is the future of netlify-cms" issue #6503
Also https://answers.netlify.com/t/is-this-project-dead/70988/61

@stale stale bot added the status: stale label Apr 26, 2023
@decaporg decaporg deleted a comment from stale bot Apr 28, 2023
@mklueh
Copy link

mklueh commented Feb 4, 2024

So as this project is not dead, but just rebranded now, can we get a standard UUID widget, please?

@doostinharrell
Copy link

Thanks for all the useful comments on this topic! I took bits a pieces from the work noted in this issue to build my own custom uuid widget for my set up. I'm using React 17, Next.js 13, Typescript, DecapCMS over the CDN, and CMS_MANUAL_INIT. Below are some code samples if anyone is struggling to adapt things for Typescript in a similar set up.

// utils/helpers.ts

import padStart from 'lodash/padStart'
import { randomFillSync } from 'crypto'

// Generate a UUID
// @returns string
export function generateUUID(): string {
  const buffer = new Uint8Array(16)
  randomFillSync(buffer)
  buffer[6] = (buffer[6] & 0x0f) | 0x40
  buffer[8] = (buffer[8] & 0x3f) | 0x80
  return (
    padStart(buffer[0].toString(16), 2, '0') +
    padStart(buffer[1].toString(16), 2, '0') +
    padStart(buffer[2].toString(16), 2, '0') +
    padStart(buffer[3].toString(16), 2, '0') +
    '-' +
    padStart(buffer[4].toString(16), 2, '0') +
    padStart(buffer[5].toString(16), 2, '0') +
    '-' +
    padStart(buffer[6].toString(16), 2, '0') +
    padStart(buffer[7].toString(16), 2, '0') +
    '-' +
    padStart(buffer[8].toString(16), 2, '0') +
    padStart(buffer[9].toString(16), 2, '0') +
    '-' +
    padStart(buffer[10].toString(16), 2, '0') +
    padStart(buffer[11].toString(16), 2, '0') +
    padStart(buffer[12].toString(16), 2, '0') +
    padStart(buffer[13].toString(16), 2, '0') +
    padStart(buffer[14].toString(16), 2, '0') +
    padStart(buffer[15].toString(16), 2, '0')
  )
}
// cms/Editor/widgets.tsx

import { Component } from 'react'
import { generateUUID } from '@/utils/helpers'

interface uuidWidgetProps {
  value: string
  onChange: (value: string) => void
}

const uuid = {
  name: 'uuid',
  control: class widgetControl extends Component<uuidWidgetProps> {
    componentDidMount() {
      const { value, onChange } = this.props
      if (!value) {
        onChange(generateUUID())
      }
    }

    render() {
      const { value } = this.props
      return <input type="text" value={value} readOnly={true} />
    }
  },
}

export default [uuid]
// pages/admin/index.tsx

// @ts-nocheck
import { useEffect } from 'react'
import Script from 'next/script'
import { config } from '@/cms/config'
import components from '@/cms/Editor/components'
import widgets from '@/cms/Editor/widgets'

const AdminPage = () => {
  useEffect(() => {
    if (typeof window !== 'undefined') {
      window.CMS_MANUAL_INIT = true

      setTimeout(() => {
        const { initCMS: init } = window

        if (typeof init !== 'undefined') {
          CMS.init({ config })

          // Import editor components
          components.forEach((component) => {
            CMS.registerEditorComponent(component)
          })

          // Import editor widgets
          widgets.forEach((widget: any) => {
            CMS.registerWidget(widget.name, widget.control)
          })
        }
      }, 1000)
    }
  }, [])

  return (
    <>
      <Script src="https://identity.netlify.com/v1/netlify-identity-widget.js" />
      <Script src="https://unpkg.com/decap-cms@3.0.12/dist/decap-cms.js" />
    </>
  )
}

export default AdminPage

@4www
Copy link

4www commented Sep 30, 2024

Iterating on @lockevn, adding a fix for when i18n is activated in a project, and we would like to generate a uuid only for the default_locale variant and when no locale

componentDidMount() {
  const {value, onChange, locale, config: {i18n} } = this.props;
  const {default_locale} = i18n;
  const defaultOrOnlyLocale = (default_locale === locale) || (!locale);
  if (!value && defaultOrOnlyLocale) {
    onChange(nanoid());
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: configuration area: ui/editor extensions/widgets/uuid pinned type: feature code contributing to the implementation of a feature and/or user facing functionality
Projects
None yet
Development

Successfully merging a pull request may close this issue.