Item maintenance list

Hi Rich, I thought, I give it (grouping/sorting/filtering items list by Thing, Tag, Semantic Class, class, hasLocation and editing items) a try but soon realized I need a kick start.

I need to get all items into an oh-repeater array. A straight forward way is to have a button which triggers a rule which calls the rest API (/rest/items) which writes the result into an item which then serves as an array for the oh-repeater.

I wonder if there is a more direct way?
I was also looking into oh-context and script component but didn’t see a way to make use of it.

The first thing I would try is to see if you can get all the Items using itemsWithTags with an itemTags left blank of set to *.

In the very likely event that that gives you no Items instead of all Items, the rule Idea is probably your best bet short of making a request to add the ability to get all Items added to the oh-repeater element.

But you don’t need to use the REST API for this. With JS Scripting you only need items.getItems() to get an array of all the Items. In Rules DSL you’ll first need to get the ItemRegistry but it has a getItems() method too. But you will probably want to expose some metadata here too and since metadata isn’t available in Rules DSL JS Scripting or jRuby might be a better choice.

@JustinG might have a better idea.

1 Like

Funny. I also tried that but it is not working.

I’ll start with the approach as mentioned above and see where this leads to and maybe others have some better ideas later on.

The itemsWithTags approach works, but you have to use:

itemsWithTags: ","

This is a rather heavy handed application of the repeater and will be somewhat slow to respond if you have a large number of items.

I have a helper widget which I include in a few other of my widgets that uses this trick. When included in another widget this will present a list of items (possibly filtered depending on configuration) and selecting each item will trigger whatever action is set via the action parameter.

uid: mod_item_select
tags: []
props:
  parameters:
    - description: Restrict list to item type
      label: Type Filter
      name: listType
      required: false
      type: TEXT
  parameterGroups:
    - name: testClick
      context: action
      label: Test Action
timestamp: Mar 19, 2023, 12:09:11 PM
component: f7-block
config:
  style:
    height: 100%
    margin: 0
    overflow-y: hidden
    padding: 0
  stylesheet: |
    .list {
      height: 100%;
      margin: 0;
      overflow-y: auto;
    }
slots:
  default:
    - component: f7-row
      config:
        style:
          background: =(themeOptions.dark=='dark')?'rgb(32,32,32)':'rgb(230,74,25)'
          height: var(--f7-navbar-height)
          position: sticky
      slots:
        default:
          - component: f7-row
            config:
              style:
                background: white
                border-radius: 5px
                height: calc(var(--f7-navbar-height) - 16px)
                margin: 5px 8px
                padding: 3px
                width: 100%
            slots:
              default:
                - component: oh-icon
                  config:
                    color: gray
                    icon: f7:search
                - component: oh-input
                  config:
                    clearButton: true
                    placeholder: "  search items"
                    style:
                      flex-grow: 10
                    type: text
                    variable: filterText
    - component: oh-list
      config: {}
      slots:
        default:
          - component: oh-repeater
            config:
              filter: (!vars.filterText || loop.ohItem.label.includes(vars.filterText) ||
                loop.ohItem.name.includes(vars.filterText)) && (!props.listType
                || loop.ohItem.type==props.listType)
              for: ohItem
              fragment: true
              itemTags: ","
              sourceType: itemsWithTags
            slots:
              default:
                - component: oh-list-item
                  config:
                    actionPropsParameterGroup: listClick
                    checkbox: true
                    checked: =props.listChecked && props.listChecked(loop.ohItem)
                    name: groupSelectRadio
                    noChevron: true
                    title: =`${loop.ohItem.label} (${loop.ohItem.name})`

Here’s an example of how it’s called (with a switch item filter):

- component: oh-link
  config:
    action: popup
    actionModal: widget:mod_item_select
    actionModalConfig:
      listChecked: =(x) => x.tags.includes((user && user.name &&
        !props.varAsGuest)?`FAV-${user.name}`:'FAV')
      listClick_action: rule
      listClick_actionRule: 8936279670
      listClick_actionRuleContext:
        modifyInfo:
          command: =`=(loop.ohItem.tags.includes((user && user.name &&
            !props.varAsGuest)?'FAV-${user.name}':'FAV'))?'REMOVE':'ADD'`
          item: =`=loop.ohItem.name`
          tag: =(user && user.name && !vars.asGuest)?`FAV-${user.name}`:'FAV'
      listType: Switch
      varAsGuest: =vars.asGuest
    iconF7: bolt_fill
1 Like

Hi Justin,
I have put together the first draft and I am struggling with the component redraw (this is related to the other question I had where you have already replied).
As you have already guessed the repeater cannot handle hundreds of items so I tried to rebuild a pagination as my goal is to keep a grid view on the items.

If I click on the pagination everything works as expected (if I manually refresh the page afterwards).
However, when adding a key to the f7-block in which the grid is defined the browser falls into an endless loop. S something must be wrong with this approach.

Could you please have a look?

If you see other areas which could be improved I’d appreciate feedback. I don’t need full code just some ideas so that I can start digging me through.

uid: item-list-table
tags: []
props:
  parameters: []
  parameterGroups: []
timestamp: Dec 16, 2024, 7:55:19 PM
component: f7-block
config:
  style:
    width: 2000px
    overflow-x: scroll
    overflow-y: scroll
    -webkit-user-select: auto
    user-select: auto
    padding: 0px
    margin: 0px
  stylesheet: |
    .grid-table {
      display: grid;
      grid-template-columns: 100px;
      grid-auto-columns: max-content;
      grid-gap: 1px;
      background-color: gray;
      padding: 0px;
      margin: 0px;
    }
    .grid-header {
      border-radius: 0px;
      font-size: 14px;
      color: white;
      position: sticky;
    }
    .input-field{
      background-color: white;
      padding: 2px;
      align-content: center;
      font-size: 14px;
    }
    .text-field{
      background-color: white;
      padding: 2px;
      font-size: 14px;
    }
slots:
  default:
    - component: oh-context
      config:
        constants:
          jsonItemList: =JSON.parse(@'vItemListFull')
          totalItems: =JSON.parse(@'vItemListFull').length
          page: =Math.floor(JSON.parse(@'vItemListFull').length / 30)
      slots:
        default:
          - component: f7-block
            config:
              class: grid-table
              _key: =Math.random().toString() + vars.varPage.toString()
            slots:
              default:
                - component: oh-repeater
                  config:
                    for: gridHeader
                    fragment: true
                    in:
                      - id: id
                        label: Id
                        isObject: false
                      - id: name
                        label: Name
                        isObject: false
                      - id: label
                        label: Label
                        isObject: false
                      - id: type
                        label: Type
                        isObject: false
                      - id: state
                        label: State
                        isObject: false
                      - id: category
                        label: Category
                        isObject: false
                      - id: groupNames
                        label: Parent Groups
                        isObject: true
                      - id: tags
                        label: (Non Semantic) Tags
                        isObject: true
                      - id: metadata
                        label: Metadata
                        isObject: true
                      - id: commandDescription
                        label: Command Options
                        isObject: true
                      - id: stateDescription
                        label: State Description
                        isObject: true
                    sourceType: array
                  slots:
                    default:
                      - component: oh-button
                        config:
                          action: variable
                          actionVariable: sortKey
                          actionVariableValue: =loop.gridHeader.id
                          text: =loop.gridHeader.label
                          class: grid-header
                          style:
                            grid-row: 1
                            grid-column: =loop.gridHeader_idx + 1
                      - component: oh-input
                        config:
                          style:
                            grid-row: 2
                            grid-column: =loop.gridHeader_idx + 1
                            background-color: lightgray
                          type: text
                          inputmode: text
                          clearButton: true
                          placeholder: search...
                          variable: ="searchKey" + loop.gridHeader.id
                          variableKey: value
                - component: oh-repeater
                  config:
                    for: gridRow
                    rangeStart: 1
                    rangeStop: 30
                    rangeStep: 1
                    fragment: true
                    sourceType: range
                  slots:
                    default:
                      - component: oh-context
                        config:
                          constants:
                            jsonItemRow: =const.jsonItemList[(((vars.varPage || 1) - 1) * 30 + (loop.gridRow
                              - 1))]
                        slots:
                          default:
                            - component: Label
                              config:
                                class: text-field
                                style:
                                  grid-row: =loop.gridRow_idx + 3
                                text: =(((vars.varPage || 1) - 1) * 30 + loop.gridRow)
                            - component: oh-input
                              config:
                                style:
                                  grid-row: =loop.gridRow_idx + 3
                                type: text
                                defaultValue: =const.jsonItemRow.name
                                variable: =(((vars.varPage || 1) - 1) * 30 + loop.gridRow) + "_newName"
                                disabled: false
                                readonly: true
                            - component: oh-input
                              config:
                                style:
                                  grid-row: =loop.gridRow_idx + 3
                                type: text
                                defaultValue: =const.jsonItemRow.label
                                variable: =(((vars.varPage || 1) - 1) * 30 + loop.gridRow) + "_newLabel"
                            - component: oh-input
                              config:
                                style:
                                  grid-row: =loop.gridRow_idx + 3
                                type: text
                                defaultValue: =const.jsonItemRow.type
                                variable: =(((vars.varPage || 1) - 1) * 30 + loop.gridRow) + "_newType"
                            - component: oh-input
                              config:
                                style:
                                  grid-row: =loop.gridRow_idx + 3
                                type: text
                                defaultValue: =const.jsonItemRow.state
                                variable: =(((vars.varPage || 1) - 1) * 30 + loop.gridRow) + "_newState"
                            - component: oh-input
                              config:
                                style:
                                  grid-row: =loop.gridRow_idx + 3
                                type: text
                                defaultValue: =const.jsonItemRow.category
                                variable: =(((vars.varPage || 1) - 1) * 30 + loop.gridRow) + "_newCategory"
                            - component: oh-input
                              config:
                                style:
                                  grid-row: =loop.gridRow_idx + 3
                                type: text
                                defaultValue: =const.jsonItemRow.groupNames
                                variable: =(((vars.varPage || 1) - 1) * 30 + loop.gridRow) + "_newGroupNames"
                            - component: oh-input
                              config:
                                style:
                                  grid-row: =loop.gridRow_idx + 3
                                type: text
                                defaultValue: =const.jsonItemRow.tags
                                variable: =(((vars.varPage || 1) - 1) * 30 + loop.gridRow) + "_newTags"
                            - component: oh-input
                              config:
                                style:
                                  grid-row: =loop.gridRow_idx + 3
                                type: text
                                defaultValue: =const.jsonItemRow.metadata
                                variable: =(((vars.varPage || 1) - 1) * 30 + loop.gridRow) + "_newMetadata"
                            - component: oh-input
                              config:
                                style:
                                  grid-row: =loop.gridRow_idx + 3
                                type: text
                                defaultValue: =const.jsonItemRow.commandDescription
                                variable: =(((vars.varPage || 1) - 1) * 30 + loop.gridRow) +
                                  "_newCommandDescription"
                            - component: oh-input
                              config:
                                style:
                                  grid-row: =loop.gridRow_idx + 3
                                type: text
                                defaultValue: =const.jsonItemRow.stateDescription
                                variable: =(((vars.varPage || 1) - 1) * 30 + loop.gridRow) +
                                  "_newStateDescription"
          - component: f7-block
            config:
              style:
                grid-column: 1 / 7
                _background-color: white
            slots:
              default:
                - component: f7-segmented
                  config:
                    color: gray
                    style:
                      margin: 5px 0px 7px 0px
                  slots:
                    default:
                      - component: oh-repeater
                        config:
                          for: rpage
                          fragment: true
                          rangeStart: 1
                          rangeStop: =const.page
                          rangeStep: 1
                          sourceType: range
                        slots:
                          default:
                            - component: oh-button
                              config:
                                action: variable
                                actionVariable: varPage
                                actionVariableValue: =loop.rpage
                                outline: true
                                raised: false
                                text: =loop.rpage
                                style:
                                  --f7-button-bg-color: =(loop.rpage == (vars.varPage || 1)) ? '#C8C8C8':''
                                  --f7-button-border-width: 1px

The problem here is that you are using the context constant for the items that are in the page. That doesn’t recalculate even it it is dependent on a variable that changes. The constants are only calculated when the oh-context is created (when the page load, or when you refresh the widget view). Instead of a constant, you want to use either of the other two context options a variable or a function. I’d probably use a function in this case. Something like:

- component: oh-context
  config:
    functions:
      getRow: =() => const.jsonItemList[(((vars.varPage || 1) - 1) * 30 + (loop.gridRow - 1))]

Then each of your grid cell starting values look like this:

defaultValue: =fn.getRow().name

With that you won’t even need to have the key value on the block, the grid will update whenever the page variable changes.

The loop is happening because of some feedback between the key and the oh-context, for reasons I cannot quite figure out. I don’t know if this is a problem with the context in general or with this specific configuration you’ve got here. I will have to do some testing. Either way, the above approach should eliminate the need for the key so you don’t run into the problem anymore.

1 Like

Thanks a lot Justin.
During “debugging” I found another potential problem with oh-context:

I double checked if the value of the two constants totalItems and page were correctly assigned when the widget is opened the first time (without the widget being redrawn).
It turns out the the values are not assigned and I get the following error message for both constants:

SyntaxError: No number after minus sign in JSON at position 1 (line 1 column 2)

It seems the consecutive parsing of one and the same item results in the value “-”.
When redrawing the widget the correct value is assigned.
This is also part of the problem, why thegrid within the widget does not get populated with items properties.

EDIT: Even the first constant (jsonItemList) is not working. Unfortunately I have also upgraded to 4.3 so I don’t know if the problem maybe comes from the new release

Here is the totally reworked widget in case you want to test it.

uid: item-list-table
tags: []
props:
  parameters: []
  parameterGroups: []
timestamp: Dec 17, 2024, 9:17:25 PM
component: f7-block
config:
  style:
    -webkit-user-select: auto
    margin: 0px
    overflow-x: scroll
    overflow-y: scroll
    padding: 0px
    user-select: auto
    width: 2000px
  stylesheet: |
    .grid-table {
      display: grid;
      grid-template-columns: 100px;
      grid-auto-columns: max-content;
      grid-gap: 1px;
      background-color: gray;
      padding: 0px;
      margin: 0px;
    }
    .grid-header {
      border-radius: 0px;
      font-size: 14px;
      color: white;
      position: sticky;
    }
    .input-field {
      background-color: white;
      padding: 2px;
      align-content: center;
      font-size: 14px;
    }
    .text-field {
      background-color: white;
      padding: 2px;
      font-size: 14px;
    }
slots:
  default:
    - component: oh-context
      config:
        functions:
          navPageId: =() => ((vars.navPageIdTmp == undefined)?1:vars.navPageIdTmp)
        constants:
          jsonItemList: =JSON.parse(@'vItemListFull')
          totalPages: =Math.ceil(JSON.parse(@'vItemListFull').length / 30)
          totalItems: =JSON.parse(@'vItemListFull').length
      slots:
        default:
          - component: f7-block
            config:
              class: grid-table
            slots:
              default:
                - component: oh-repeater
                  config:
                    for: gridHeader
                    fragment: true
                    in:
                      - id: id
                        isObject: false
                        label: Id
                      - id: name
                        isObject: false
                        label: Name
                      - id: label
                        isObject: false
                        label: Label
                      - id: type
                        isObject: false
                        label: Type
                      - id: state
                        isObject: false
                        label: State
                      - id: category
                        isObject: false
                        label: Category
                      - id: groupNames
                        isObject: true
                        label: Parent Groups
                      - id: tags
                        isObject: true
                        label: (Non Semantic) Tags
                      - id: metadata
                        isObject: true
                        label: Metadata
                      - id: commandDescription
                        isObject: true
                        label: Command Options
                      - id: stateDescription
                        isObject: true
                        label: State Description
                    sourceType: array
                  slots:
                    default:
                      - component: oh-button
                        config:
                          action: variable
                          actionVariable: sortKey
                          actionVariableValue: =loop.gridHeader.id
                          class: grid-header
                          style:
                            grid-column: =loop.gridHeader_idx + 1
                            grid-row: 1
                          text: =loop.gridHeader.label
                      - component: oh-input
                        config:
                          clearButton: true
                          inputmode: text
                          placeholder: search...
                          style:
                            background-color: lightgray
                            grid-column: =loop.gridHeader_idx + 1
                            grid-row: 2
                          type: text
                          variable: ="searchKey_" + loop.gridHeader.id
                          variableKey: value
                - component: oh-repeater
                  config:
                    for: r_gridRow
                    fragment: true
                    rangeStart: =((fn.navPageId() - 1) * 30)
                    rangeStep: 1
                    rangeStop: =(fn.navPageId() * 30)
                    sourceType: range
                  slots:
                    default:
                      - component: oh-context
                        config:
                          functions:
                            gridRow: =() => const.jsonItemList[loop.r_gridRow]
                        slots:
                          default:
                            - component: Label
                              config:
                                class: text-field
                                style:
                                  grid-row: =loop.r_gridRow_idx + 3
                                text: =loop.r_gridRow + 1
                            - component: oh-input
                              config:
                                defaultValue: =const.jsonItemList[loop.r_gridRow].name
                                disabled: false
                                readonly: true
                                style:
                                  grid-row: =loop.r_gridRow_idx + 3
                                type: text
                                variable: =loop.r_gridRow + "_name"
                            - component: oh-input
                              config:
                                defaultValue: =const.jsonItemList[loop.r_gridRow].label
                                style:
                                  grid-row: =loop.r_gridRow_idx + 3
                                type: text
                                variable: =loop.r_gridRow + "_label"
                            - component: oh-input
                              config:
                                defaultValue: =const.jsonItemList[loop.r_gridRow].type
                                style:
                                  grid-row: =loop.r_gridRow_idx + 3
                                type: text
                                variable: =loop.r_gridRow + "_type"
                            - component: oh-input
                              config:
                                defaultValue: =const.jsonItemList[loop.r_gridRow].state
                                style:
                                  grid-row: =loop.r_gridRow_idx + 3
                                type: text
                                variable: =loop.r_gridRow + "_state"
                            - component: oh-input
                              config:
                                defaultValue: =const.jsonItemList[loop.r_gridRow].category
                                style:
                                  grid-row: =loop.r_gridRow_idx + 3
                                type: text
                                variable: =loop.r_gridRow + "_category"
                            - component: oh-input
                              config:
                                defaultValue: =const.jsonItemList[loop.r_gridRow].groupNames
                                style:
                                  grid-row: =loop.r_gridRow_idx + 3
                                type: text
                                variable: =loop.r_gridRow + "_groupNames"
                            - component: oh-input
                              config:
                                defaultValue: =const.jsonItemList[loop.r_gridRow].tags
                                style:
                                  grid-row: =loop.r_gridRow_idx + 3
                                type: text
                                variable: =loop.r_gridRow + "_tags"
                            - component: oh-input
                              config:
                                defaultValue: =const.jsonItemList[loop.r_gridRow].metadata
                                style:
                                  grid-row: =loop.r_gridRow_idx + 3
                                type: text
                                variable: =loop.r_gridRow + "_metadata"
                            - component: oh-input
                              config:
                                defaultValue: =const.jsonItemList[loop.r_gridRow].commandDescription
                                style:
                                  grid-row: =loop.r_gridRow_idx + 3
                                type: text
                                variable: =loop.r_gridRow + "_commandDescription"
                            - component: oh-input
                              config:
                                defaultValue: =const.jsonItemList[loop.r_gridRow].stateDescription
                                style:
                                  grid-row: =loop.r_gridRow_idx + 3
                                type: text
                                variable: =loop.r_gridRow + "_stateDescription"
          - component: f7-block
            config:
              style:
                background-color: white
            slots:
              default:
                - component: f7-segmented
                  config:
                    color: gray
                    style:
                      margin: 5px 0px 7px 0px
                  slots:
                    default:
                      - component: oh-repeater
                        config:
                          for: r_page
                          fragment: true
                          rangeStart: 1
                          rangeStep: 1
                          rangeStop: =const.totalPages
                          sourceType: range
                        slots:
                          default:
                            - component: oh-button
                              config:
                                action: variable
                                actionVariable: navPageIdTmp
                                actionVariableValue: =loop.r_page
                                outline: true
                                raised: false
                                style:
                                  --f7-button-bg-color: =(loop.r_page == fn.navPageId()) ? '#C8C8C8':''
                                  --f7-button-border-width: 1px
                                  width: 50px
                                text: =loop.r_page
                - component: Label
                  config:
                    text: ="fn.navPageId; " + fn.navPageId()
                - component: Label
                  config:
                    text: ="vars.navPageIdTmp; " + vars.navPageIdTmp
                - component: Label
                  config:
                    text: ="const.totalPages; " + const.totalPages.toString()
                - component: Label
                  config:
                    text: ="const.totalItems; " + const.totalItems.toString()

I don’t think this is related to the upgrade. This sounds very similar to another issue I’ve already reported with the context:

I haven’t had time recently to follow up with that (and won’t for a little while yet).

I see. Thanks for that. So I just need to add a button „Load data“ to overcome this bug. No problem. Would it make a difference if I loadad json data via rest API call? Not sure though if I can perform an http request within an oh-context.

That’s not quite the solution for two reasons. First there’s no way for you to capture the results of the “load” in the widget expression context, and second if that button is on the same page, that it still happening after the constant values have been set.

The workaround that I talk about in the issue is that some other page has to reference the item under normal conditions (not an oh-context constant). This causes the item to be added to the tracking list. Then if you go to your page after opening that other page everything works as intended.

Or, just be OK with opening the page, leaving it and opening it again, which also works because after the constants are evaluated the first time the the item is in the tracking list so the next time you open the page everything works as it should.

Also, the key value oh-context feedback loop that you encountered appears to be a general bug and not your code. It can be replicated with a very simple widget.

1 Like

Just like to add I found another workaround. Simply adding the item as a default value to a widget’s props section.

1 Like

That’s great thinking. It also may be a useful data point when it comes to developing a solution to this issue.

Is there a way to control, i.e. adding a prefix to the return value of an oh-input to an item?
I know there is a variable but I cannot trigger a rule or store its value in an item.

Not that I am aware of.

1 Like