These are chat archives for GetmeUK/ContentTools

22nd
Feb 2018
Abishek R Srikaanth
@abishekrsrikaanth
Feb 22 2018 15:27
hey @anthonyjb, just checking in to see if you have any updates on ContentFlow?
Matthew J. Sahagian
@mattsah
Feb 22 2018 19:03
@anthonyjb playing around with ContentFlow right now, and I'm a bit confused trying to put together the way this works by looking at the Mock API and the Base API... the thing I'm confused about is the flow id is getting passed for getting both snippet types and getting snippets... this is confusing cause I would think getting snippets based on a flow id would require a universally unique id, but getting snippet types would be more of a grouping. The mock API uses article-body for example, presumably you can have a data-cf-flow="article-body" on multiple pages... so then it's not clear how getting snippets based on that ID is supposed to differentiate.
Anthony Blackshaw
@anthonyjb
Feb 22 2018 19:17

@mattsah so the API accepts baseParams which is what we use to set the Id of the current page the user is editing, the flow name is used:

  • to determine what snippet types should be returned (so different snippet types are available for different flows typically)
  • to determine which flow to store a snippet in, so we're using mongodb and the page will have a flows field which is a dictionary containing one or more flows where each flow is stored using the name of the flow as a key

I don't know if this is any use to you but this is the source code we're using in manhattan to implement saving of content flows (Coffee I'm afraid - well if you're not a fan that is):

Despite the name; FlowAPI is actually an unmodified version of BaseAPI (just inherits to be future safe there's no modifications).

$ = require 'manhattan-essentials'
ContentTools = require 'ContentTools'
ContentFlow = require 'content-flow'
FlowAPI = require('./api.coffee').FlowAPI
ImageUploader = require('./image-uploader.coffee').ImageUploader

class SaveSemaphore
    # Simple semaphore to manage the busy state of the editor when saving
    # changes.

    @_saving = 0

    @dec: () ->
        # Decrement the semaphore count
        @_saving = Math.max(0, @_saving - 1)

    @inc: () ->
        # Increment the semaphore count
        @_saving += 1

    @saving: () ->
        # Return true if the editor is still saving
        return @_saving > 0

addSaveProcess = (url, params, buildContentsFunc) ->
    # Set up a save process based on the content built using the given
    # function.
    editor = ContentTools.EditorApp.get()

    editor.addEventListener 'saved', (ev) ->

        # If nothing has changed exit early
        regions = ev.detail().regions
        unless Object.keys(regions).length > 0
            return

        # Build the contents object we'll be saving
        contents = buildContentsFunc(regions)

        # If we have no content changes to save exit early
        unless Object.keys(contents).length > 0
            return

        # Mark the editor as busy while we save the page
        SaveSemaphore.inc()
        editor.busy(true)

        # Create a save request
        xhr = new XMLHttpRequest()
        xhr.open('POST', url)

        # Add the form data for the save request
        formData = new FormData()
        for k, v of (params or {})
            formData.append(k, v)
        formData.append('contents', JSON.stringify(contents))
        xhr.send(formData)

        # Handle the save request response
        xhr.addEventListener 'load', (ev) ->

            # Check if were still saving changes and if not mark the editor as
            # no longer busy.
            SaveSemaphore.dec()
            unless SaveSemaphore.saving()
                editor.busy(false)

            # Handle the result of the save request
            if parseInt(ev.target.status) is 200
                new ContentTools.FlashUI('ok')
            else
                new ContentTools.FlashUI('no')

init = (baseURL='/', baseFlowURL='/', baseParams={}) ->

    # Set-up the editor and flow manager
    editor = ContentTools.EditorApp.get()
    flowMgr = ContentFlow.FlowMgr.get()

    # Use minimal whitespace in the HTML output as it's easier to deal with
    ContentEdit.INDENT = ''
    ContentEdit.LINE_ENDINGS = ''

    # Restrict special attributes
    ContentTools.RESTRICTED_ATTRIBUTES['*'].push('data-ce-tag')
    ContentTools.RESTRICTED_ATTRIBUTES['*'].push('data-fixture')
    ContentTools.RESTRICTED_ATTRIBUTES['*'].push('data-transforms')
    ContentTools.RESTRICTED_ATTRIBUTES['*'].push('data-mh-asset-key')
    ContentTools.RESTRICTED_ATTRIBUTES['*'].push('data-name')

    # Assign an image uploader for the editor
    ContentTools.IMAGE_UPLOADER = (dialog) ->
        return new ImageUploader(dialog, baseParams)

    # Initialize the editor
    editor.init(
        '[data-allow-edits] [data-editable], [data-allow-edits] [data-fixture]',
        'data-name'
    )

    # Initialize the content flow manager
    api = new FlowAPI(baseFlowURL, baseParams)
Matthew J. Sahagian
@mattsah
Feb 22 2018 19:20
I saw baseParams and I expected as much for the page... but this would still mean then you could only have one flow of each "type" (flow "type" that is) on each page, right? So like in the sandbox, you couldn't have two article-body flows, right?
Anthony Blackshaw
@anthonyjb
Feb 22 2018 19:20
you can have as many flows on the page as you want
in fact we have snippet types that add flows into the page dynamically
Matthew J. Sahagian
@mattsah
Feb 22 2018 19:21
I understand that
but how does it differentiate between one article-body and another?
Anthony Blackshaw
@anthonyjb
Feb 22 2018 19:21
so you mean if you're editing flows for 2 different database records?
Matthew J. Sahagian
@mattsah
Feb 22 2018 19:22
You said flow name is typically used... to determine which flow to store a snippet in
so if I had <article data-cf-flow="article-body"> </article> twice on a page...
e.g. two articles were on the page
I don't follow how it would be differentiating which article body is containing which snippets
Anthony Blackshaw
@anthonyjb
Feb 22 2018 19:26
right so that's not something we implement but if you wanted to there's a couple of approaches you might take, for example you could make the Id part for the flow name and parse it out (server-side or client in the API) (this is how we separate each of the snippets).
We use a colon to separate for example

in fact that's probably the approach I'd take as it keeps everything unique on the page, otherwise you'd need to modify some of the package functions like getFlowDOMelement.

But if you're uncomfortable with that approach you can override these methods including getFlowCls which would allow you to manage the data against your flow and also the API I/O fully.

Matthew J. Sahagian
@mattsah
Feb 22 2018 19:31
@anthonyjb trying to keep it as close to upstream as possible... it just seemed weird case data-cf-flow seemed to serve that dual purpose of determining available snippets, but also acting as an id for the flow on the page
That just seems to make it fairly complex for a snippet that would have a flow in it...
Anthony Blackshaw
@anthonyjb
Feb 22 2018 19:34

well for a single document case e.g one page, where you have a dictionary containing flows, this works well - but I agree it works less well if you are managing the same flow across multiple documents via one page.

This is my page model in manhattan, the flows field here stores a dict of potentially many flows, each one keyed by the flow name

class Page(PublishableFrame):
    """
    A page on the website.
    """

    _fields = {
        'name',
        'name_lower',
        'slug',
        'template',
        'flows'
    }

    _uncompared_fields = PublishableFrame._uncompared_fields | {
        'name_lower'
    }

    _indexes = [
        IndexModel([('slug', ASC)], unique=True),
        IndexModel([('name', ASC)], unique=True)
    ]

    _default_projection = {
        'flows': {'$sub.': Snippet}
    }

    def __str__(self):
        return self.name

    def validate_publish(self):
        # Ensure the page's name and slug is unique within the published
        # context.
        with Page._context_manager.published():
            query = And(
                Or(
                    Q.slug == self.slug,
                    Q.name_lower == self.name_lower
                ),
                Q._id != self._id
            )
            if Page.count(query) > 0:
                raise PublishingError('The page slug is not unique')

    @property
    def is_index(self):
        # Return True if this page is set as the site index
        return self.slug == 'index'

    @property
    def template_name(self):
        return inflection.humanize(self.template[:-5])

    @property
    def path(self):
        if self.is_index:
            return '/'
        return '/' + self.slug

    @property
    def url(self):
        return path_to_url(self.path)

    @classmethod
    def get_templates(cls):
        """
        Return a dictionary of templates paths and humanized template names
        available to use for pages.
        """

        # Get a list of template filenames with the directory
        template_filenames = next(os.walk(cls.get_templates_path()))[2]

        # Build the templates table
        templates = {}
        for template_filename in template_filenames:

            # Make sure the filename ends in .html otherwise ignore it
            if not template_filename.lower().endswith('.html'):
                continue

            # Generate a human friendly name for the template
            name = inflection.humanize(template_filename[:-5])

            # Store the template in the table
            templates[template_filename] = name

        return templates

    @classmethod
    def get_templates_path(cls):
        """Return a path to the directory containing page templates"""
        return os.path.join(
            os.path.split(__file__)[0],
            'public/templates/pages'
        )

    @classmethod
    def path_for(cls, name, default=None):
        """Return the path for a page based on the given name"""
        page = Page.one(
            Q.name_lower == name.lower(),
            projection={'slug': True}
        )
        if page:
            return page.path
        return default

    @staticmethod
    def _on_upsert(sender, frames):
        for frame in frames:

            # Set the lower case version of the name
            if frame.name:
               frame.name_lower = frame.name.lower()


Page.listen('insert', Page._on_upsert)
Page.listen('update', Page._on_upsert)
if a snippet has a flow name in it then it will tend to look like a little like this:
<div
    class="text-content"
    data-cf-snippet="{{ snippet.id }}"
    style="padding-top: 0;"
    >
    <div class="text-content__inner  |  inner">
        <div class="text-content__text">

            <form
                class="contact__form | form"
                method="POST"
                style="max-width: 600px;"
                >

                <fieldset
                    class="form__fieldset"
                    data-cf-flow="form__fields:{{ snippet.id }}"
                    data-cf-flow-label="Form"
                    >
                    {% if page %}
                        {% for snippet in page.flows['form__fields:' + snippet.id] %}
                            {{ snippet.render()|safe }}
                        {% endfor %}
                    {% endif %}
                </fieldset>

                <button
                    class="form__btn"
                    type="submit"
                    >
                    <span class="form__btn-label">Send</span>
                </button>
            </form>

        </div>
    </div>
</div>
and then on the server side we have pattern rules that allow us to simply say this snippet type is acceptable for all flow names that begin with 'form__fields', e.g 'form__fields:*'
Anthony Blackshaw
@anthonyjb
Feb 22 2018 19:40
@mattsah @abishekrsrikaanth byw I'm really sorry I haven't made more progress on the docs, we have some huge projects on until the end of April and whilst ContentTools/Flow are part of those most of my time is currently on the related ManhattanCMS project which is all used heavily by these on-going projects
Matthew J. Sahagian
@mattsah
Feb 22 2018 19:41
@anthonyjb no real worry, by and large the mock API and the base API give a pretty good idea of what gets sent and what needs to be returned
it's just this rather complex case that boggled my mind
Anthony Blackshaw
@anthonyjb
Feb 22 2018 19:42
@mattsah if you ever want access to a fully working demo let me know your email and I'll set you up some login details so you can have a play and see what's going on request wise
Matthew J. Sahagian
@mattsah
Feb 22 2018 19:44
Will do, for now I think I'm good though. Thanks
Anthony Blackshaw
@anthonyjb
Feb 22 2018 19:45
:thumbsup: