Spartacus Configurable Product Integration: Custom Normalizer nested data being overridden - spartacus-storefront

I am trying to implement a custom normalizer to the configurable product feature module. I have to include a custom field in the Attributes datatype. Currently only the OccConfigurationVariantNormalizer is available, which is quite high level form a data's point of view.
My problem occurs with the execution order of the normalizers. The default normalizer ist this: https://github.com/SAP/spartacus/blob/develop/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-normalizer.ts which is being called after my custom normalizer. Hence, the convertGroup() function is overriding my custom attribute field.
Here is my implementation:
#Injectable({
providedIn: 'root'
})
export class CustomConfiguratorNormalizerService extends OccConfiguratorVariantNormalizer{
convertAttribute(sourceAttribute: CustomOccAttribute, attributeList: CustomAttribute[]): void{
super.convertAttribute(sourceAttribute, attributeList);
attributeList[attributeList.length - 1].customField = sourceAttribute.customField;
}
}
Extending the original Normalizer seemed like the most promising solution for the time being, and is working quite like intended. So the customField ist being present at this point in time of execution.
Afterwards the OccConfiguratorVariantNormalizer kicks in, which is defining a new Attribute array in convertGroup(), erasing my custom attribute:
convertGroup([...]) {
const attributes: Configurator.Attribute[] = [];
if (source.attributes) {
source.attributes.forEach((sourceAttribute) =>
this.convertAttribute(sourceAttribute, attributes)
);
}
[...]
};
convertAttribute(
sourceAttribute: OccConfigurator.Attribute,
attributeList: Configurator.Attribute[]
): void {
const attribute: Configurator.Attribute = {
name: sourceAttribute.name,
label: sourceAttribute.langDepName,
required: sourceAttribute.required,
uiType: this.convertAttributeType(sourceAttribute.type),
values: [],
groupId: this.getGroupId(sourceAttribute.key, sourceAttribute.name),
userInput: sourceAttribute.formattedValue,
maxlength:
sourceAttribute.maxlength + (sourceAttribute.negativeAllowed ? 1 : 0),
numDecimalPlaces: sourceAttribute.numberScale,
negativeAllowed: sourceAttribute.negativeAllowed,
numTotalLength: sourceAttribute.typeLength,
selectedSingleValue: null,
images: [],
hasConflicts: sourceAttribute?.conflicts?.length > 0 ? true : false,
};
[...]
};
If my custom normalizer was the only one I could imagine it would work, which is why I tried to inject it like this:
{
provide: VARIANT_CONFIGURATOR_NORMALIZER,
useClass: CustomConfiguratorNormalizerService,
multi: false,
}
Throwing me Error: Multi-providers mixed with single providers.
Also, using the documentation from https://sap.github.io/spartacus-docs/connecting-to-other-systems/ I cannot get it to work without extending the original Normalizer, since target will always be undefined, which probably would not be the case if my custom normalizer came in second.
I feel like this https://github.com/SAP/spartacus/issues/9046 could be related.
Any help very much appreciated :)

I was able to solve this myself. Following the reference structure for spartacus applications at https://sap.github.io/spartacus-docs/reference-app-structure/ the problem disappeared.
My best guess is that it has to do with the import order of the modules. In my current working version I import the FeaturesModule last, which seems to solve the problem.

Related

Docusaurus: How can I have multiple versions of different docs in the docs directory?

I'm working with Docusaurus to create a documentation site for 3 different education courses - all within the docs folder.
So I'm looking for a way to have the version be different across folders in there, or figure out what the best strategy for this is.
Right now, in my docusaurus.config.js I have:
module.exports = {
presets: [
'#docusaurus/preset-classic',
docs: {
lastVersion: 'current',
versions: {
current: {
label: '1.0.0',
path: '1.0.0',
},
},
},
],
};
But I'm not sure how to keep track of 3 different versions across 3 different docs all within the same site.
Swizzle the navbar via wrapping
yarn run swizzle #docusaurus/theme-classic NavbarItem/DocsVersionDropdownNavbarItem -- --wrap
Modify the swizzled component like so:
src/theme/NavbarItem/DocsVersionDropdownNavbarItem.js:
import React from "react";
import DocsVersionDropdownNavbarItem from '#theme-original/NavbarItem/DocsVersionDropdownNavbarItem';
import { useLocation } from '#docusaurus/router';
export default function DocsVersionDropdownNavbarItemWrapper(props) {
const { docsPluginId, className, type } = props
const { pathname } = useLocation()
/* (Custom) check if docsPluginId contains pathname
Given that the docsPluginId is 'charge-controller' and the routeBasePath is 'charge-controller', we can check against the current URI (pathname).
If the pathname contains the docsPluginId, we want to show the version dropdown. Otherwise, we don't want to show it.
This gives us one, global, context-aware version dropdown that works with multi-instance setups.
You want to declare a version dropdown for each plugin in your navbarItems config property for this to work well.
const doesPathnameContainDocsPluginId = pathname.includes(docsPluginId)
if (!doesPathnameContainDocsPluginId) {
return null
}
return <DocsVersionDropdownNavbarItem {...props} />;
}
For this to work, you need to have your documentation (based on products) split up using multi-instances: (https://docusaurus.io/docs/docs-multi-instance#docs-navbar-items)
Note that the preset docsPlugin ID always is "default".
You can try to use
import {
useActivePluginAndVersion,
} from '#docusaurus/plugin-content-docs/client';
const version = activePluginAndVersion.activeVersion.name; // use label instead of name if issues arise.
instead to get the current docsPluginId, name or label.
This would be the more "robust" solution I think. That said, we do use the solution I provided above as-is and it works fine for now.

fire event from external controller

i'm moving to Rails 7 and i feel like there are so many changes but i'm confident on understanding them and be able to upgrade a personal applications i made myself for keeping my personal records and appointments
More specific i need to communicate between controllers (#hotwire/stimulus) between a flatpickr controller and fullcalendar contorller. The idea is to jump to a date when selecting from flatpicr
I've tried so many diferent options but i'm really stuck.. any help is welcome :)
Rails 7.0.3.1
index.html.erb
<div data-controller="flatpickr" name="" data-action=""></div>
<div data-controller="calendar">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
flatpickr_controller.js
import Flatpickr from 'stimulus-flatpickr'
export default class extends Flatpickr {
connect() {
this.config = {
inline: true,
enableTime: false,
time_24hr: false,
onChange: function(selectedDates, dateStr, instance) {
const calendarController = this.application.getControllerForElementAndIdentifier(this.calendarTarget, "calendar")
calendarController.gotoDate('18-01-2025') //random date
},
};
super.connect();
}
}
calendar_controller.js
import { Controller } from "#hotwired/stimulus";
import { Calendar } from '#fullcalendar/core';
import resourceTimeGridPlugin from '#fullcalendar/resource-timegrid';
import interactionPlugin from '#fullcalendar/interaction';
export default class extends Controller {
static targets = [ "popup", "window" ];
connect() {
let overlay = this.popupTarget;
this.calendar = new Calendar(this.windowTarget, {
plugins: [ resourceTimeGridPlugin, interactionPlugin ],
themeSystem: 'bootstrap5',
initialView: 'resourceTimeGridDay',
aspectRatio: 1.8,
nowIndicator: true,
selectable: true,
editable: true,
allDaySlot: false,
});
window.addEventListener('load', () => {
this.calendar.render();
});
}
refresh(e) {
if (e.detail.success) {
this.calendar.refetchEvents();
}
}
}
output
application-7082a89999639e6d01ae0ef0aaaf6707b39fab96541f1dcd1c79da24753cb0ed.js:28271 Uncaught TypeError: Cannot read properties of undefined (reading 'getControllerForElementAndIdentifier')
at Object.onChange (ap ...
I think I'm gonna get mad with this... thank you!
Well done on trying to understand all of this, it can be hard to learn something new and especially when you have 'working' code and you are kind of forced to change.
One thing that can help is to revisit the Stimulus documentation, it does have pretty much all the answers you need for these issues but maybe needs a bit of a re-read.
The other thing which can be super frustrating is JavaScript's usage of this and how it works.
Hopefully the below breakdown helps.
Problems
1. Understanding this (JavaScript)
The first problem with the code above is that you are referencing this with the assumption that it refers to your controller instance, but rather it is referring to the event's context.
onChange: function(selectedDates, dateStr, instance) {
const calendarController = this.application.getControllerForElementAndIdentifier(this.calendarTarget, "calendar")
calendarController.gotoDate('18-01-2025') //random date
},
In the above code, this.application and this.calendarTarget will never work as the this here is the context created by the onChange handler calling context.
The quick way around this this issue is to just use an arrow function. In the below revised code snippet (which will still not work, due to issues 2 & 3 below), the arrow function approach is used instead of a function declaration, which pulls in the this from the parent context, which will be the Controller's instance.
onChange: (selectedDates, dateStr, instance) => {
const calendarController = this.application.getControllerForElementAndIdentifier(this.calendarTarget, "calendar")
calendarController.gotoDate('18-01-2025') //random date
},
The best way, however, is to read the documentation on Mozilla here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this in full, read it again and then maybe a third time. After that, find some YouTube videos and watch those. You will find JavaScript development much easier if you truly 'grok' this concept, but it is hard to understand.
2. Understanding Stimulus Targets
The next issue is your use of this.calendarTarget in your flatpackr controller, this controller will not have any target available due to it not being set up correctly.
In the Stimulus docs - https://stimulus.hotwired.dev/reference/targets you can read that the target must be in the controller's scope. But in the HTML below the data-controller="flatpickr" div has no children and also has no targets in the HTML anywhere that can be accessed by this controller.
<div data-controller="flatpickr" name="" data-action="">No Children here?</div>
<div data-controller="calendar">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
There are a few ways to access something outside the controller's scope, but the easiest way would be to bypass this problem all together and use the Stimulus' preferred way to communicate with other controllers.
But, if you want to use a target you need to do two things.
A. Ensure the target static attribute is declared on your controller.
export default class extends Flatpickr {
static targets = [ "calendar" ]; // this is required
B. Ensure the target element has the right attribute and is a child of the desired controller.
<div data-controller="flatpickr" name="" data-action="">
<div data-controller="calendar" data-flatpickr-target="calendar">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
</div>
3. Stimulus Cross-Controller Coordination With Events
Finally, your use of getControllerForElementAndIdentifier is documented as a work around if there is no other way to communicate with another controller.
The preferred way is using events and it is incredibly powerful, flexible and will probably solve 99.9% of your use cases. Have a read of https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent & https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent if you are not sure what browser events are first.
Then, you can dispatch an event from your flatpackr controller for your calendar controller to pick up on.
The simplest way to go about this is an event that jut gets dispatched on your first controller and 'bubbles' up the DOM and then your calendar controller listens to this globally.
Solution - Example Code
First, start with your HTML, the only real change below is the data-action attribute on your calendar.
This will listen for a global event flatpackr:changed and when it sees that, it will call your calendar controller's method goToDate.
<div data-controller="flatpickr"></div>
<div data-controller="calendar" data-action="flatpackr:changed#window->calendar#goToDate">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
In your flatpackr controller, using the arrow function approach described above, you can call this.dispatch() which will trigger the dispatching of a CustomEvent with the supplied options.
Stimulus will prefix the name supplied with the controller's name.
Note: You could be more specific with where this event gets dispatched to, but if there is only ever going to be one calendar instance, no need to worry for now.
import Flatpickr from 'stimulus-flatpickr'
export default class extends Flatpickr {
connect() {
this.config = {
inline: true,
enableTime: false,
time_24hr: false,
onChange: (selectedDates, dateStr, instance) => {
// note: Stimulus sets `bubbles` to true by default but good to be explicit
const someDate = '18-01-2025'; // random date
// passing data to the other controller can be via the `detail` object in the CustomEvent & Stimulus will automatically add 'flatpackr:' to the start of the event name for you (Thanks Stimulus!)
this.dispatch('changed', { detail: { date: someDate } , bubbles: true } );
},
};
// super.connect(); - not sure that you need this in most cases so commented out
}
}
In the calendar controller, all that is needed is the method to be declared goToDate.
You can read the supplied detail in the event.detail from the param.
import { Controller } from "#hotwired/stimulus";
import { Calendar } from '#fullcalendar/core';
import resourceTimeGridPlugin from '#fullcalendar/resource-timegrid';
import interactionPlugin from '#fullcalendar/interaction';
export default class extends Controller {
static targets = [ "popup", "window" ];
connect() {
// ...
// note: you may not need the window on load listener as `connect` will only be called when there is a DOM ready to attach to.
}
refresh(e) {
// ...
}
goToDate(event) {
// note: you can use destructuring above and change the signature to ` goToDate({ detail: { date } }) {` instead
const date = event.detail.date;
console.log('do something with the date now', date);
}
}
Note: I have not tested locally but should be close enough

Is it possible to query document schema metadata in Sanity/GROQ?

I have a simple singleton document schema defined in my Sanity/NextJS project, to model my "Colophon" page (richText is a custom block field type):
export default {
title: 'Colophon',
name: 'colophon',
type: 'document',
__experimental_actions: ['update', 'publish'],
fields: [
{
title: 'Body',
name: 'body',
type: 'richText',
validation: Rule => Rule.required(),
},
],
};
I retrieve this document with a simple query in my NextJS application:
export async function getStaticProps() {
const colophon = await client.fetch(`
*[_type == "colophon"][0]
`);
// ...
};
Is it possible to write a GROQ query to retrieve the meta title defined in the schema, i.e. Colophon? Although this is a singleton document, I would like to avoid repeating this string in my project if possible. At the moment, I can only see the fields on the document in my results, i.e. body.
Thanks for reading!
No, I don't believe there is.
As far as I understand what you're after; The schema is defined in the studio-instance and not the datastore. Those two are not hard coupled. I have several studio-instances with small variations on the schemas using one single project/datastore. The API you query to get data does not care which studio and schema was used and cant answer for the actual schema details.

OpenUI5 sap.m.Input Currency Formatting

This looks to be answered many different times but I can't seem to get it working with my implementation. I am trying to format and limit the data in a sap.m.Input element. I currently have the following:
var ef_Amount = new sap.m.Input({
label: 'Amount',
textAlign: sap.ui.core.TextAlign.Right,
value: {
path: '/amount',
type: 'sap.ui.model.type.Currency'
}
});
The first problem is that it kind of breaks the data binding. When I inspect the raw data (with Fiddler) submitted to the server it is an array like this:
"amount": [1234.25,null]
The server is expecting a single number and as such has issues with the array.
When I use the following, the binding works as desired but no formatting is performed.
var ef_Amount = new sap.m.Input({
label: 'Amount',
textAlign: sap.ui.core.TextAlign.Right,
value: '{/amount}'
});
The second problem is that the data entered is not limited to numbers.
I have tried using sap.m.MaskedInput instead but I don't like the usage of the placeholders because I never know the size of the number to be entered.
And lastly, it would be nice if when focus is placed on the input field, that all formatting is removed and re-formatted again when focus lost.
Should I be looking into doing this with jQuery or even raw Javascript instead?
Thank you for looking.
the array output is a normal one according to documentation. So you need to teach your server to acccept this format or preprocess data before submission;
this type is not intended to limit your data input;
good feature, but ui5 does not support this, because the Type object has no idea about control and it's events like "focus" it only deals with data input-output. So you have to implement this functionality on your own via extending the control or something else.
I would suggest using amount and currency separately. It's likely that user should be allowed to enter only valid currency, so you can use a combobox with the suggestions of the available currencies.
So, after much work and assistance from #Andrii, I managed to get it working. The primary issue was that onfocusout broke the updating of the model and the change event from firing. Simply replacing onfocusout with onsapfocusleave took care of the issues.
The final code in the init method of my custom control:
var me = this;
var numberFormat = sap.ui.core.NumberFormat.getCurrencyInstance({maxFractionDigits: 2});
me.addEventDelegate({
onAfterRendering: function() {
// for formatting the figures initially when loaded from the model
me.bindValue({
path: me.getBindingPath('value'),
formatter: function(value) {
return numberFormat.format(value);
}
});
},
onfocusin: function() {
// to remove formatting when the user sets focus to the input field
me.bindValue(me.getBindingPath('value'));
},
onsapfocusleave: function() {
me.bindValue({
path: me.getBindingPath('value'),
formatter: function(value) {
return numberFormat.format(value);
}
});
}
});

Creating and removing Ember Models dynamically / at runtime

I have been working on this for a couple of days now and have decided to bite the bullet and ask for help.
I am using ember-cli, and I'm trying to create Ember Models dynamically at runtime based on some metadata. An sample use case would be if a user wanted to specify a data set (query, file etc.); I would like to read the user's data file and setup a model based on the file's structure.
I have successfully setup an ember-cli application which uses Ember Data and Fixtures (for simplicity). My static example has three models and I can see the models and the data in the Store when I use the Chrome Ember debugger.
Now on to my dynamic example...
I have been able to successfully define a model at runtime by setting it up in IndexRoute
var IndexRoute = Ember.Route.extend({
init: function() {
// Create a simple model dynamically
var MyModel = DS.Model.extend({
defineAttributes: function(){
Ember.defineProperty(this, 'col1', DS.attr());
Ember.defineProperty(this, 'col2', DS.attr());
Ember.defineProperty(this, 'col3', DS.attr());
}.on('init')
});
// insert some data into our model
MyModel.reopenClass({
FIXTURES: [
{
id: '1',
col1: "key1",
col2: "value1",
col3: "value2"
},
{
id: '2',
col1: "key2",
col2: "value1",
col3: "value2"
},
{
id: '3',
col1: "key3",
col2: "value1",
col3: "value2"
}
]
});
}
});
The problem is that I don't see where the model has been placed, and it doesn't appear in the Chrome debugger in the Data (store).
If I change the definition of MyModel to Ember.Application.MyModel then I can see the Model is defined under Ember.Application in the console but it still isn't visible to the Ember Debugger.
I then tried to extend the above example to add the model hook as follows:
var IndexRoute = Ember.Route.extend({
init: function() {
...
},
model: function() {
return this.store.findAll('my-model');
}
});
But the call to this.store.findAll('my-model') fails in both cases. I am assuming that I am not wiring things up correctly.
In my example I am also aware that the name of my Model is not dynamic either so I really want to know how to create a name for my model programmatically at runtime.
I would really appreciate some help or guidance from those out there who are more experienced with the framework because I've been really impressed with Ember and the whole configuration over convention approach.
Thanks,
Dave
You're essentially defining something, then throwing it away. You'll need to attach it to your app's namespace (you can see it in your index file or configuration file if you're using Ember-cli).
Additionally, I doubt defining attributes on init will work, it might, but it isn't actually necessary.
Thirdly, do you really want to use Ember Data for this? Ember Data is very opinionated, and it sounds like you don't want to define things.