Require tips on implementing @mention tag functionality in Appian in text and paragraph fields?

Certified Senior Developer

The Business wants to implement a functionality where, a User Tag can be added to a note [paragraph field] to a new/existing record. What is the best way to achieve this without having to perform a lot of post-processing in the system.

Example: [Text] @UserName1 [Some More Text] @UserName2 [Additional Text]

Should trigger emails to the mentioned users about the note added.


Current Suggested Solution: have a user picker where user can tag users and below the user can add the notes in paragraph field. Which most likely be rejected due to extra clicks, and it might also cause some ambiguity otherwise requiring tagged users to be mentioned again in the added note itself.

  Discussion posts and replies are publicly visible

Parents
  • To add to your options here, this is an OOTB example that could definitely be combined with regex as Stefan notes, for enhanced tag parsing, etc.  

    There are some interesting logic questions here such as determining your username if you allow periods (as we do, e.g. first.last accounts), determining if the period is part of the user or the end of a sentence.  The supporting cleaner rule here will ignore the period if there is an unallowable (non alpha-numeric or period) character after it, but include if the following character is allowable.  Regex would also make it quicker and easier to validate things such as, integers are allowed but only at the end of the username.  Otherwise this example basically splits the input text on "@", determines where the end of each tag is via cleaner rule and returns a dictionary with keys "tag", "isUser" (for validation), "startIndex" and "length".  One situation it does not handle yet is returning the proper index if the username is tagged more than once.  It also could be expanded to create a richTextDisplayField which replaces the tags with links (user profile, email, etc).  Give it a whirl.

    Supporting cleaner rule:

    a!localVariables(
      local!text: lower(ri!text),
      local!allowed: "abcdefghijklmnopqrstuvwxyz.0123456789",
      local!allowedList: a!forEach(items: 1+enumerate(len(local!allowed)),expression: index(local!allowed,fv!index)),
      local!cleaned: a!forEach(
        items: 1+enumerate(len(local!text)),
        expression: {
          a!localVariables(
            local!char: index(local!text,fv!index,null),
            if(contains(local!allowedList,local!char),local!char," ")
          )
        }
      ),
      local!output: left(
        ri!text,
        index(
          wherecontains(
            " ",
            a!forEach(local!cleaned,tostring(fv!item))
          ),
          1,
          len(ri!text)+1
        )-1
      ),
      
      if(
        right(local!output,1)=".",
        left(local!output,len(local!output)-1),
        local!output
      )
    )

    Interface:

    a!localVariables(
      local!text,
      local!tags: if(rule!APN_isEmpty(local!text),"",
        reject(
          fn!isnull,
          a!forEach(
            items: split(local!text,"@"),
            expression: {
              if(
                fv!index=1, /* ignore anything before the first @ */
                null,
                a!localVariables(
                  local!tag: rule!chris_test_inline_tags_clean(text: fv!item), /* call the cleaner rule */
                  {
                    tag: local!tag,
                    isUser: isusernametaken(local!tag),
                    startIndex: search(local!tag,local!text,1),
                    length: len(local!tag)
                  },
                )
              )
            }
          )
        )
      ),
      a!columnsLayout(
        columns: {
          a!columnLayout(
            contents: {
              a!paragraphField(
                label: "Text",
                value: local!text,
                saveInto: local!text
              ),
              a!paragraphField(
                label: "Tags",
                value: local!tags,
                readOnly: true
              ),
            }
          ),
          a!columnLayout()
        }
      )
    )

Reply
  • To add to your options here, this is an OOTB example that could definitely be combined with regex as Stefan notes, for enhanced tag parsing, etc.  

    There are some interesting logic questions here such as determining your username if you allow periods (as we do, e.g. first.last accounts), determining if the period is part of the user or the end of a sentence.  The supporting cleaner rule here will ignore the period if there is an unallowable (non alpha-numeric or period) character after it, but include if the following character is allowable.  Regex would also make it quicker and easier to validate things such as, integers are allowed but only at the end of the username.  Otherwise this example basically splits the input text on "@", determines where the end of each tag is via cleaner rule and returns a dictionary with keys "tag", "isUser" (for validation), "startIndex" and "length".  One situation it does not handle yet is returning the proper index if the username is tagged more than once.  It also could be expanded to create a richTextDisplayField which replaces the tags with links (user profile, email, etc).  Give it a whirl.

    Supporting cleaner rule:

    a!localVariables(
      local!text: lower(ri!text),
      local!allowed: "abcdefghijklmnopqrstuvwxyz.0123456789",
      local!allowedList: a!forEach(items: 1+enumerate(len(local!allowed)),expression: index(local!allowed,fv!index)),
      local!cleaned: a!forEach(
        items: 1+enumerate(len(local!text)),
        expression: {
          a!localVariables(
            local!char: index(local!text,fv!index,null),
            if(contains(local!allowedList,local!char),local!char," ")
          )
        }
      ),
      local!output: left(
        ri!text,
        index(
          wherecontains(
            " ",
            a!forEach(local!cleaned,tostring(fv!item))
          ),
          1,
          len(ri!text)+1
        )-1
      ),
      
      if(
        right(local!output,1)=".",
        left(local!output,len(local!output)-1),
        local!output
      )
    )

    Interface:

    a!localVariables(
      local!text,
      local!tags: if(rule!APN_isEmpty(local!text),"",
        reject(
          fn!isnull,
          a!forEach(
            items: split(local!text,"@"),
            expression: {
              if(
                fv!index=1, /* ignore anything before the first @ */
                null,
                a!localVariables(
                  local!tag: rule!chris_test_inline_tags_clean(text: fv!item), /* call the cleaner rule */
                  {
                    tag: local!tag,
                    isUser: isusernametaken(local!tag),
                    startIndex: search(local!tag,local!text,1),
                    length: len(local!tag)
                  },
                )
              )
            }
          )
        )
      ),
      a!columnsLayout(
        columns: {
          a!columnLayout(
            contents: {
              a!paragraphField(
                label: "Text",
                value: local!text,
                saveInto: local!text
              ),
              a!paragraphField(
                label: "Tags",
                value: local!tags,
                readOnly: true
              ),
            }
          ),
          a!columnLayout()
        }
      )
    )

Children
  • I know usernames are allowed to include a period, but are they allowed to end with one?  If not, that would probably make the regex quite a bit simpler to write, off the topf of my head (i haven't parsed what you've provided above so you might already handle this somehow).  Just a thought.

  • The example above is ignoring periods at the end of a username, however my 20.3 admin console did just allow me to create a test account as "testuser." with a trailing period..  Not sure if there are any business situations that would utilize accounts with period at the end, but I guess it is system allowed.  Which does make the tag cleaning more annoying Slight smile

  • Updated Interface example which pulls out the valid users (listed on the right side):

    a!localVariables(
      local!text,
      local!tags: if(rule!APN_isEmpty(local!text),"",
        reject(
          fn!isnull,
          a!forEach(
            items: split(local!text,"@"),
            expression: {
              if(
                fv!index=1, /* ignore anything before the first @ */
                null,
                a!localVariables(
                  local!tag: rule!chris_test_inline_tags_clean(text: fv!item), /* call the cleaner rule */
                  {
                    tag: local!tag,
                    isUser: isusernametaken(local!tag),
                    startIndex: search(local!tag,local!text,1),
                    length: len(local!tag)
                  },
                )
              )
            }
          )
        )
      ),
      a!columnsLayout(
        columns: {
          a!columnLayout(
            contents: {
              a!paragraphField(
                label: "Text",
                value: local!text,
                saveInto: local!text
              ),
              a!paragraphField(
                label: "Tags",
                value: local!tags,
                readOnly: true
              ),
            }
          ),
          a!columnLayout(
            contents: {
              a!richTextDisplayField(
                label: "User List",
                value: {
                  a!forEach(
                    items: local!tags,
                    expression: {
                      if(
                        fv!item.isUser,
                        {
                          a!richTextItem(
                            showWhen: fv!item.isUser,
                            text: touser(fv!item.tag)
                          ),
                          char(13)
                        },
                        {}
                      )
                    }
                  )
                }
              )
            }
          )
        }
      )
    )