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

Allow to create IDocuments from Linked items #21

Open
Simply007 opened this issue Nov 4, 2020 · 13 comments · Fixed by #23
Open

Allow to create IDocuments from Linked items #21

Simply007 opened this issue Nov 4, 2020 · 13 comments · Fixed by #23
Assignees
Labels
enhancement New feature or request

Comments

@Simply007
Copy link
Contributor

Is your feature request related to a problem? Please describe.
If you want to load Linked Items of the particular item, it is required to transform them into IDocument to be able to pass them through the pipeline and i.e. use them to create a view model for the Razor view. It would be great to have this feature out of the box.

I will describe the situation in the example:
You have a "Root" item that is the root of your website, this contains the Linked Items element (Subpages) model menu structure. Every item (Page type) then contains another Subpages element to allow having a multilevel navigation menu. Plus the Page model contains "Content" Linked items element containing typically one item holding the channel-agnostic content.
This is basically a Webspotlight setup.

Example model structure:

public partial class Root
{
        public IEnumerable<object> Subpages { get; set; } // Strongly types items of type i.e. Page
}

public partial class Page
{
        public IEnumerable<object> Subpages { get; set; } // Strongly types items of type i.e. Page
        public IEnumerable<object> Content { get; set; } // Contains only one "Content" item e.g. LandingPage
}

public partial class LandingPage
{
        public string Headline { get; set; }
        public IRichTextContent MainText { get; set; }
        public IRichTextContent Summary { get; set; }
}

Now if you want to load the data from Root and render data from the linked items in razor views, you would do something like:

InputModules = new ModuleList {
                new Kontent<Root>(client)
                    .WithQuery(
                        new EqualsFilter("system.codename", "root"),
                        new LimitParameter(1),
                        new DepthParameter(3)
                    ),
                new ReplaceDocuments(
                    new ExecuteConfig(
                        Config.FromDocument((doc, context) => 
                             // *********** THIS PART COULD BE PART OF THE  Kontent.Statiq MODULE ************ 
                            doc.AsKontent<Root>().Subpages.ToList().Select(subpage =>
                            {
                                var pageContent = (subpage as Page)?.Content.FirstOrDefault();
                                if(pageContent == null)
                                {
                                        throw new InvalidDataException("Root page (codename: root, type: root) does not contain any pages, or any page does not contain exactly content block!");
                                }

                                return context.CreateDocument(
                                    CreateKontentDocument(context, pageContent)); 
                            })
                        )
                    )
                )
            };

// ...

ProcessModules = new ModuleList {
                new MergeContent(new ReadFiles("LandingPage.cshtml")),
                new RenderRazor()
                    .WithModel(Config.FromDocument((document, context) =>
                    {
                        var typeCodename = document
                            .FilterMetadata(KontentKeys.System.Type)
                            .Values
                            ?.FirstOrDefault()
                            ?.ToString();
                        Type type = typeProvider.GetType(typeCodename);

                        var landingPageType = typeof(LandingPage);
                        if(landingPageType == type)
                        {
                                return document.AsKontent<LandingPage>();
                        }
                        else
                        {
                                throw new InvalidDataException("Unsuported content type of the Page's Content element");
                        }
                    })),
    // ****
    };


// basically copy&paste of the https://github.com/alanta/Kontent.Statiq/blob/main/Kontent.Statiq/Kontent.cs#L49
private IDocument CreateKontentDocument(IExecutionContext context, object item)
        {
            var props = item.GetType().GetProperties(BindingFlags.Instance | BindingFlags.FlattenHierarchy |
                                                            BindingFlags.GetProperty | BindingFlags.Public);
            var metadata = new List<KeyValuePair<string, object>>
            {
                new KeyValuePair<string, object>(TypedContentExtensions.KontentItemKey, item),
            };

            if (props.FirstOrDefault(prop => typeof(IContentItemSystemAttributes).IsAssignableFrom(prop.PropertyType))
                ?.GetValue(item) is IContentItemSystemAttributes systemProp)
            {
                metadata.AddRange(new[]
                {
                    new KeyValuePair<string, object>(KontentKeys.System.Name, systemProp.Name),
                    new KeyValuePair<string, object>(KontentKeys.System.CodeName, systemProp.Codename),
                    new KeyValuePair<string, object>(KontentKeys.System.Language, systemProp.Language),
                    new KeyValuePair<string, object>(KontentKeys.System.Id, systemProp.Id),
                    new KeyValuePair<string, object>(KontentKeys.System.Type, systemProp.Type),
                    new KeyValuePair<string, object>(KontentKeys.System.LastModified, systemProp.LastModified)
                });
            }

            return context.CreateDocument(metadata, null, "text/html");
        }

In the CreateKontentDocument I have only removed the part of GetContent delegate for simplicity and using item.GetType().GetProperties instead of typeof<TContentModel>.GetProperties

Describe the solution you'd like

It would be great to have the functionality to transfer Linked Items to IEnumerable<IDocument> right in the SDK.

doc.LinkedItemsAsKontentDocuments<Root>(root => root.Subpages);

Describe alternatives you've considered
I was thinking about a separate module, but I think it is overengineering.

Additional context
I have the solution in the https://github.com/Kentico/jamstackon.net/blob/linked-items-idocument-transformation/Pipelines/RootPipeline.cs

  • I have invited you to the project @alanta
@alanta alanta self-assigned this Nov 4, 2020
@alanta alanta added the enhancement New feature or request label Nov 4, 2020
@alanta
Copy link
Owner

alanta commented Nov 6, 2020

@petrsvihlik the KontentModelGenerator currently outputs releated pages as IEnumerable<object>. Will it stay like that or will that ever change to explicitly typed collections?

@petrsvihlik
Copy link
Contributor

@alanta we track an issue for that here: kontent-ai/model-generator-net#90

We could overcome this drawback with a Management API key, as described at kontent-ai/model-generator-net#90 (comment).

Since there's not going to be a new version of the Delivery API any time soon, it seems like a good approach (if we keep it optional).

@petrsvihlik
Copy link
Contributor

But why are you asking? What's your use-case?

@alanta
Copy link
Owner

alanta commented Nov 8, 2020

@petrsvihlik I was trying to figure out if it was possible to automatically map related content as child documents in the Statiq IDocument metadata. But it seems I'd need more info than is available through reflection.

@petrsvihlik
Copy link
Contributor

Would the explicitly typed collections solve your problem?

alanta added a commit that referenced this issue Nov 9, 2020
* KontentConfig makes it a bit easier to work with Statiq Config and Kontent
* Child documents can now be axtracted through KontentConfig.GetChildren, issue #21
* Added AddKontentDocumentsToMetadata module to make it even easier to work with releated content
* Moved mapping from Kontent models to IDocument into KontentDocumentHelpers
@alanta
Copy link
Owner

alanta commented Nov 9, 2020

@Simply007 I've pushed an initial implementation. I've implemented basically what you've proposed. I have tried to make it convenient to work with both typed content and still be able to leverage the all the goodness provided by Statiq.

The core of it is that I've introduced a KontentConfig class with helpers to construct Statiq Config declarations aimed at Kontent content. For example, this code will add documents from a collection if linked content to the document metadata:

new Kontent<Root>(client)
                    .WithQuery(
                        new LimitParameter(1),
                        new DepthParameter(3)
                    ),
new AddDocumentsToMetadata(Keys.Children, KontentConfig.GetChildren<Root>(page => page.Subpages)),
// or, with the convenience module:
new AddKontentDocumentsToMetadata<Root>(page => page.Subpages),

This gives you a document hierarchy and at a later moment you'd be able to pull the child pages out again through the standard Statiq methods.

Now for your exact example, where you're replacing the parent document with it's children, this should do it:

new Kontent<Root>(client)
                    .WithQuery(
                        new LimitParameter(1),
                        new DepthParameter(3)
                    ),
// dig up the content and set that as the pipeline documents
new ReplaceDocuments( KontentConfig.GetChildren<Root>( page => page.SubPages.OfType<Page>()
        .Content.OfType<LandingPage>() ) ),

Since KontentConfig.GetChildren should handle null values and empty collections gracefully, this single expression should be enough to handle most of the code in your example. The output of that step will contain only pages of the LandingPage type.

Then to finish it off, this should render that landing page content:

new MergeContent(new ReadFiles("LandingPage.cshtml")),
new RenderRazor()
          .WithModel( KontentConfig.As<LandingPage>() )

Note how the KontentConfig.As<T> helper makes the expression a lot easier to grok.

@alanta
Copy link
Owner

alanta commented Nov 9, 2020

@petrsvihlik For now, I've decided not to try to map out the document hierarchy automatically. There doesn't seem to be an easy way to do delayed expansion of child document collections. So any automatic expansion has the potential to become an epic fail with lots of unnecessary procssing.

The proposed solution requires the dev to specify what collections to break out into Statiq document metadata. But I think that's reasonable.

@petrsvihlik
Copy link
Contributor

@alanta I think that's better. The important part is that we support it, we can always improve the support later.

@Simply007
Copy link
Contributor Author

I have done some testing and everything looks fine!

I have been playing mainly around web spotlight model and now I think it is OK to use.

Basically, I have created a pipeline that loads the "Root" of the web spotlight content model, and based on the content pages types linked in the "tree" structure (just one level) it loads the template, prepares the view model, and defines the destination file based on the data.

https://github.com/Kentico/jamstackon.net/blob/test-linked-items/Pipelines/RootPipeline.cs


There is one "tricky" part. Apart from the "Content" page (in the example LandingPage content type) the data required during the rendering might be stored in Page˙ type, like URL slug (showcased in the sample), Show in the menu, .... So flattening the list of input files to just a list of "Content" pages is not sufficient.

https://github.com/Kentico/jamstackon.net/blob/test-linked-items/Pipelines/RootPipeline.cs#L40

The current implementation is using SetMetadata to append the extra information to Metadata of the Content pages under specific COntent page, and it should be possible to use this approach even if you need the data from multiple level of the menu, but it does not look really nice.

https://github.com/Kentico/jamstackon.net/blob/test-linked-items/Pipelines/RootPipeline.cs#L32


Ideally, if there was a way to link the parents and then from child call something like document.AsKontent().GetParent()... it would be great. But i think this is a bit complicated and out of scope if this issue, I would vote to split this to a separate issue.

@alanta
Copy link
Owner

alanta commented Nov 11, 2020

Maybe @daveaglick can shine a light on this particular scenario. I'm not quite sure what the best way is to deal with hierarchical data like this in Statiq.

@alanta
Copy link
Owner

alanta commented Nov 11, 2020

@Simply007 One more thing I introduced in this commit is an easier way to get values from the Kontent data model. So you should be able to replace this:

 new SetMetadata(URL_PATH_KEY, Config.FromDocument((doc, ctx) =>
{                        
   return doc.AsKontent<Page>().Url;
})),

with this

new SetMetadata(URL_PATH_KEY, KontentConfig.Get( (Page page) => page.Url ))

It's essentially the same but the KontentConfig helper removes a lot of the noise, making the code easier to read.

I'm going to push out a new release with what we have now.

@alanta alanta mentioned this issue Nov 11, 2020
5 tasks
alanta added a commit that referenced this issue Nov 11, 2020
* Support for child documents + general API improvements

* KontentConfig makes it a bit easier to work with Statiq Config and Kontent
* Child documents can now be axtracted through KontentConfig.GetChildren, issue #21
* Added AddKontentDocumentsToMetadata module to make it even easier to work with releated content
* Moved mapping from Kontent models to IDocument into KontentDocumentHelpers
@alanta
Copy link
Owner

alanta commented Nov 11, 2020

Oops, the PR closed the issue. I think we still need to figure out how to work with document hierarchies.

@alanta alanta reopened this Nov 11, 2020
@Simply007
Copy link
Contributor Author

Hello @alanta,

We are at the final stage of releasing the first version of the know types linked items generator: kontent-ai/model-generator-net#165 => If you want, reach out on https://kontent.ai/discord/ and we can discuss this further.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants