diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index ef7cc8b..d07e565 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -10,6 +10,17 @@ Neos: groups: default: label: Query Arguments + # This package provides a custom LinkEditor which allows editing linking options. You can disable this by + # setting the replace option to false + linkEditor: + replace: true + # Some linking options don't operate on the link string and don't make sense for the LinkEditor. They should + # never be enabled + ignoredOptions: + - title + - targetBlank + - relNoFollow + - download fusion: autoInclude: Prgfx.Neos.LinkEditor: true diff --git a/README.md b/README.md index 4b54a90..ff018e7 100644 --- a/README.md +++ b/README.md @@ -79,3 +79,17 @@ Prgfx: UriBuilder: true ConvertUris: true ``` + +## Note +This package overwrites the default LinkEditor to allow passing additional linking options to enable the parameter editor in the inspector editor. +You can disable this behavior in the settings: +```yaml +Neos: + Neos: + Ui: + frontendConfiguration: + Prgfx.Neos.LinkEditor: + linkEditor: + replace: false +``` +See the package settings for more information diff --git a/Resources/Private/LinkEditor/src/components/LinkEditor.tsx b/Resources/Private/LinkEditor/src/components/LinkEditor.tsx new file mode 100644 index 0000000..3277e8b --- /dev/null +++ b/Resources/Private/LinkEditor/src/components/LinkEditor.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { GlobalNeos } from '../util/useNeos'; +import { fromEntries } from '../util/objects'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { LinkInput } from '@neos-project/neos-ui-editors'; + +type LinkEditorProps = { + value?: string; + commit: (newValue: string) => void; + options?: { + [key: string]: unknown, + linking?: Record, + }; + neos: GlobalNeos; +} + +/** + * Custom wrapper around the LinkEditor to support linking options in the inspector + * @param props + * @constructor + */ +export const LinkEditor: React.FunctionComponent = props => { + const { linking, ...options } = props.options; + + const editorConfiguration = props.neos.globalRegistry + .get('frontendConfiguration') + .get<{linkEditor?: { ignoredOptions: string[] }}>('Prgfx.Neos.LinkEditor'); + const ignoredOptions = editorConfiguration.linkEditor?.ignoredOptions ?? []; + + const linkingOptions = { + ...linking, + ...fromEntries(ignoredOptions.map(option => [ option, false ])), + }; + + return ( + + ); +}; diff --git a/Resources/Private/LinkEditor/src/manifest.ts b/Resources/Private/LinkEditor/src/manifest.ts index 29df6de..a86b79b 100644 --- a/Resources/Private/LinkEditor/src/manifest.ts +++ b/Resources/Private/LinkEditor/src/manifest.ts @@ -1,7 +1,9 @@ import manifest from '@neos-project/neos-ui-extensibility'; import { LinkAttributeEditor } from './components/LinkAttributeEditor'; +import { LinkEditor } from './components/LinkEditor'; +import { Registry } from './util/useNeos'; -manifest('Prgfx.Neos.LinkEditor:LinkEditor', {}, (globalRegistry) => { +manifest('Prgfx.Neos.LinkEditor:LinkEditor', {}, (globalRegistry: Registry, { frontendConfiguration }) => { const containerRegistry = globalRegistry.get('containers'); containerRegistry.set( @@ -10,10 +12,19 @@ manifest('Prgfx.Neos.LinkEditor:LinkEditor', {}, (globalRegistry) => { 'end' ); + const editorSettings: {linkEditor?: { replace: boolean }} = frontendConfiguration['Prgfx.Neos.LinkEditor']; + if (editorSettings.linkEditor.replace) { + const editorRegistry = globalRegistry.get('inspector').get('editors'); + editorRegistry.set('Neos.Neos/Inspector/Editors/LinkEditor', { + component: LinkEditor, + }); + } + + type DataLoader = { resolveValue: (options: unknown, identifier: string) => unknown }; // we generate links with query arguments, but we want to ignore these arguments when looking up the node, so it // will still be displayed in the link editor - const linkLookupDataLoader = globalRegistry.get('dataLoaders').get('LinkLookup'); - const originalResolve: (options: unknown, identifier: string) => unknown = linkLookupDataLoader.resolveValue; + const linkLookupDataLoader = globalRegistry.get('dataLoaders').get('LinkLookup'); + const originalResolve = linkLookupDataLoader.resolveValue; const newResolve: typeof originalResolve = (options, oldLookup) => { const newLookup = typeof oldLookup === 'string' && /^node:\/\//.test(oldLookup) ? oldLookup.split('?')[0] diff --git a/Resources/Private/LinkEditor/src/util/objects.ts b/Resources/Private/LinkEditor/src/util/objects.ts index a383a83..3fb1bab 100644 --- a/Resources/Private/LinkEditor/src/util/objects.ts +++ b/Resources/Private/LinkEditor/src/util/objects.ts @@ -1,4 +1,4 @@ -export const fromEntries = (items: [string, string][]): Record => +export const fromEntries = (items: [string, T][]): Record => items.reduce((carry, [ key, value ]) => { carry[key] = value; return carry; diff --git a/Resources/Private/LinkEditor/src/util/useNeos.ts b/Resources/Private/LinkEditor/src/util/useNeos.ts index fec9518..17f122a 100644 --- a/Resources/Private/LinkEditor/src/util/useNeos.ts +++ b/Resources/Private/LinkEditor/src/util/useNeos.ts @@ -3,14 +3,15 @@ import { useContext } from 'react'; // @ts-ignore import { NeosContext } from '@neos-project/neos-ui-decorators'; -type Registry = { +export type Registry = { get(key: string): T; + set(key: string, value: T, position?: string): void; } -type NeosResponse = { +export type GlobalNeos = { globalRegistry: Registry; } -export const useNeos = (): NeosResponse => { +export const useNeos = (): GlobalNeos => { return useContext(NeosContext); }; diff --git a/Resources/Public/Plugin/LinkEditor/Plugin.js b/Resources/Public/Plugin/LinkEditor/Plugin.js index fdfbcd2..0254d8f 100644 --- a/Resources/Public/Plugin/LinkEditor/Plugin.js +++ b/Resources/Public/Plugin/LinkEditor/Plugin.js @@ -1 +1 @@ -(()=>{var $=Object.create;var L=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var Y=Object.getOwnPropertyNames;var q=Object.getPrototypeOf,J=Object.prototype.hasOwnProperty;var Q=(t,e)=>()=>(t&&(e=t(t=0)),e);var x=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var X=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of Y(e))!J.call(t,o)&&o!==n&&L(t,o,{get:()=>e[o],enumerable:!(r=z(e,o))||r.enumerable});return t};var g=(t,e,n)=>(n=t!=null?$(q(t)):{},X(e||!t||!t.__esModule?L(n,"default",{value:t,enumerable:!0}):n,t));function p(t){return(...e)=>{if(window["@Neos:HostPluginAPI"]&&window["@Neos:HostPluginAPI"][`@${t}`])return window["@Neos:HostPluginAPI"][`@${t}`](...e);throw new Error("You are trying to read from a consumer api that hasn't been initialized yet!")}}var m=Q(()=>{});var h=x((St,S)=>{m();S.exports=p("vendor")().React});var C=x((jt,_)=>{m();_.exports=p("NeosProjectPackages")().ReactUiComponents});var K=x((Ut,V)=>{m();V.exports=p("NeosProjectPackages")().NeosUiDecorators});m();var v=p("manifest");var l=g(h());var I=t=>{let e={};return!t||typeof t!="object"||Array.isArray(t)||Object.entries(t).forEach(([n,r])=>{if(!r||typeof r!="object"||Array.isArray(r)||!("label"in r)||typeof r.label!="string")return;let o="collapsed"in r&&r.collapsed===!0;e[n]={key:n,label:r.label,collapsed:o}}),e},E="default",N=t=>{if(!("linkAttributes"in t)||!t.linkAttributes||typeof t.linkAttributes!="object"||Array.isArray(t.linkAttributes))return[];let e=[];return Object.keys(t.linkAttributes).forEach(n=>{let r=t.linkAttributes[n];if(!r)return;if(typeof r=="string")return e.push({attribute:n,label:r,group:E});if(typeof r!="object"||Array.isArray(r)||!("label"in r&&typeof r.label=="string"))return;let o="help"in r&&typeof r.help=="string"?r.help:void 0,u="placeholder"in r&&typeof r.placeholder=="string"?r.placeholder:void 0,i="group"in r&&typeof r.group=="string"?r.group:E;return e.push({attribute:n,label:r.label,help:o,placeholder:u,group:i})}),e},j=t=>{let e=t.reduce((n,r)=>({...n,[r.group]:[...n[r.group]??[],r]}),{});return Object.entries(e)};var w=t=>t.reduce((e,[n,r])=>(e[n]=r,e),{});var a=g(h());var f=g(h()),d=g(C()),W=({inputId:t,option:e,i18nRegistry:n})=>{let[r,o]=(0,f.useState)(!1),u=()=>o(i=>!i);return f.default.createElement("div",null,f.default.createElement("div",{style:{display:"flex",justifyContent:"space-between"}},f.default.createElement(d.Label,{htmlFor:t},n.translate(e.label)),e.help&&f.default.createElement("span",{role:"button",onClick:u},f.default.createElement(d.Icon,{icon:"question-circle"}))),e.help&&r&&f.default.createElement(d.Tooltip,{renderInline:!0},n.translate(e.help)))};var A=g(C());var tt=0,P=()=>`prgfx-le-id${++tt}`;var et={padding:8,border:0,margin:0,position:"relative",width:"100%"},rt={display:"block",top:8,marginBottom:8,position:"relative"},nt={position:"absolute",top:"-3ch",right:0},it=t=>({display:t?"none":"flex",flexWrap:"wrap",gap:8}),F=t=>t.collapsed===!0&&t.options.every(({attribute:e})=>!(e in t.values)||t.values[e]===""),G=t=>{let[e,n]=(0,a.useState)(F(t)),r=(0,a.useRef)(!1),o=P(),u=P();return(0,a.useEffect)(()=>{r.current||Object.keys(t.values).length>0&&(n(F(t)),r.current=!0)},[t.values]),a.default.createElement("fieldset",{style:et},a.default.createElement("legend",{style:rt,id:o},t.i18nRegistry.translate(t.label)),a.default.createElement("div",{style:nt},a.default.createElement(A.IconButton,{icon:e?"chevron-circle-down":"chevron-circle-up",onClick:()=>n(i=>!i),style:"transparent",size:"small","aria-expanded":!e,"aria-controls":u,"aria-labelledby":o})),a.default.createElement("div",{style:it(e),id:u},t.options.map((i,b)=>{let y=`link-attribute-input-${b}`;return a.default.createElement("div",{key:i.attribute},a.default.createElement(W,{option:i,inputId:y,i18nRegistry:t.i18nRegistry}),a.default.createElement(A.TextInput,{id:y,value:t.values[i.attribute],onChange:t.onChange(i.attribute),placeholder:t.i18nRegistry.translate(i.placeholder)}))})))};var U=g(h()),M=g(K()),T=()=>(0,U.useContext)(M.NeosContext);var H=t=>{let{onLinkChange:e}=t,{globalRegistry:n}=T(),r=(0,l.useMemo)(()=>{let s=n.get("frontendConfiguration").get("Prgfx.Neos.LinkEditor");return I(s.groups)},[n]),[o,u]=(0,l.useState)({}),i=(0,l.useMemo)(()=>N(t.linkingOptions),[t.linkingOptions]),b=(0,l.useMemo)(()=>j(i).filter(([s])=>s in r),[i,r]);(0,l.useEffect)(()=>{try{let s=new URL(t.linkValue);s.search&&(s.searchParams,u(w(i.map(c=>[c.attribute,s.searchParams.get(c.attribute)??""]))))}catch{u(w(i.map(c=>[c.attribute,""])))}},[t.linkValue,i]);let y=(0,l.useCallback)(s=>c=>{u(D=>{let B={...D,[s]:c};try{let k=new URL(t.linkValue),O=k.searchParams;c.length===0&&O.has(s)?O.delete(s):O.set(s,c),e(k.toString())}catch{}return B})},[t.linkValue,e]);return l.default.createElement(l.default.Fragment,null,b.map(([s,c])=>l.default.createElement(G,{key:s,label:r[s].label,i18nRegistry:t.i18nRegistry,options:c,onChange:y,values:o,collapsed:r[s].collapsed})))};v("Prgfx.Neos.LinkEditor:LinkEditor",{},t=>{t.get("containers").set("LinkInput/OptionsPanel/LinkAttributeEditor",H,"end");let n=t.get("dataLoaders").get("LinkLookup"),r=n.resolveValue,o=(u,i)=>{let b=typeof i=="string"&&/^node:\/\//.test(i)?i.split("?")[0]:i;return r.call(n,u,b)};n.resolveValue=o.bind(n)});})(); +(()=>{var Q=Object.create;var w=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var Z=Object.getOwnPropertyNames;var tt=Object.getPrototypeOf,et=Object.prototype.hasOwnProperty;var rt=(t,e)=>()=>(t&&(e=t(t=0)),e);var A=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var nt=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of Z(e))!et.call(t,o)&&o!==n&&w(t,o,{get:()=>e[o],enumerable:!(r=X(e,o))||r.enumerable});return t};var g=(t,e,n)=>(n=t!=null?Q(tt(t)):{},nt(e||!t||!t.__esModule?w(n,"default",{value:t,enumerable:!0}):n,t));function c(t){return(...e)=>{if(window["@Neos:HostPluginAPI"]&&window["@Neos:HostPluginAPI"][`@${t}`])return window["@Neos:HostPluginAPI"][`@${t}`](...e);throw new Error("You are trying to read from a consumer api that hasn't been initialized yet!")}}var b=rt(()=>{});var y=A((_t,v)=>{b();v.exports=c("vendor")().React});var C=A((Ft,_)=>{b();_.exports=c("NeosProjectPackages")().ReactUiComponents});var T=A((Bt,F)=>{b();F.exports=c("NeosProjectPackages")().NeosUiDecorators});var B=A((te,H)=>{b();H.exports=c("NeosProjectPackages")().NeosUiEditors});b();var R=c("manifest");var u=g(y());var N=t=>{let e={};return!t||typeof t!="object"||Array.isArray(t)||Object.entries(t).forEach(([n,r])=>{if(!r||typeof r!="object"||Array.isArray(r)||!("label"in r)||typeof r.label!="string")return;let o="collapsed"in r&&r.collapsed===!0;e[n]={key:n,label:r.label,collapsed:o}}),e},S="default",I=t=>{if(!("linkAttributes"in t)||!t.linkAttributes||typeof t.linkAttributes!="object"||Array.isArray(t.linkAttributes))return[];let e=[];return Object.keys(t.linkAttributes).forEach(n=>{let r=t.linkAttributes[n];if(!r)return;if(typeof r=="string")return e.push({attribute:n,label:r,group:S});if(typeof r!="object"||Array.isArray(r)||!("label"in r&&typeof r.label=="string"))return;let o="help"in r&&typeof r.help=="string"?r.help:void 0,l="placeholder"in r&&typeof r.placeholder=="string"?r.placeholder:void 0,i="group"in r&&typeof r.group=="string"?r.group:S;return e.push({attribute:n,label:r.label,help:o,placeholder:l,group:i})}),e},j=t=>{let e=t.reduce((n,r)=>({...n,[r.group]:[...n[r.group]??[],r]}),{});return Object.entries(e)};var k=t=>t.reduce((e,[n,r])=>(e[n]=r,e),{});var a=g(y());var d=g(y()),h=g(C()),G=({inputId:t,option:e,i18nRegistry:n})=>{let[r,o]=(0,d.useState)(!1),l=()=>o(i=>!i);return d.default.createElement("div",null,d.default.createElement("div",{style:{display:"flex",justifyContent:"space-between"}},d.default.createElement(h.Label,{htmlFor:t},n.translate(e.label)),e.help&&d.default.createElement("span",{role:"button",onClick:l},d.default.createElement(h.Icon,{icon:"question-circle"}))),e.help&&r&&d.default.createElement(h.Tooltip,{renderInline:!0},n.translate(e.help)))};var O=g(C());var it=0,E=()=>`prgfx-le-id${++it}`;var st={padding:8,border:0,margin:0,position:"relative",width:"100%"},at={display:"block",top:8,marginBottom:8,position:"relative"},lt={position:"absolute",top:"-3ch",right:0},ut=t=>({display:t?"none":"flex",flexWrap:"wrap",gap:8}),V=t=>t.collapsed===!0&&t.options.every(({attribute:e})=>!(e in t.values)||t.values[e]===""),W=t=>{let[e,n]=(0,a.useState)(V(t)),r=(0,a.useRef)(!1),o=E(),l=E();return(0,a.useEffect)(()=>{r.current||Object.keys(t.values).length>0&&(n(V(t)),r.current=!0)},[t.values]),a.default.createElement("fieldset",{style:st},a.default.createElement("legend",{style:at,id:o},t.i18nRegistry.translate(t.label)),a.default.createElement("div",{style:lt},a.default.createElement(O.IconButton,{icon:e?"chevron-circle-down":"chevron-circle-up",onClick:()=>n(i=>!i),style:"transparent",size:"small","aria-expanded":!e,"aria-controls":l,"aria-labelledby":o})),a.default.createElement("div",{style:ut(e),id:l},t.options.map((i,m)=>{let f=`link-attribute-input-${m}`;return a.default.createElement("div",{key:i.attribute},a.default.createElement(G,{option:i,inputId:f,i18nRegistry:t.i18nRegistry}),a.default.createElement(O.TextInput,{id:f,value:t.values[i.attribute],onChange:t.onChange(i.attribute),placeholder:t.i18nRegistry.translate(i.placeholder)}))})))};var K=g(y()),U=g(T()),D=()=>(0,K.useContext)(U.NeosContext);var M=t=>{let{onLinkChange:e}=t,{globalRegistry:n}=D(),r=(0,u.useMemo)(()=>{let s=n.get("frontendConfiguration").get("Prgfx.Neos.LinkEditor");return N(s.groups)},[n]),[o,l]=(0,u.useState)({}),i=(0,u.useMemo)(()=>I(t.linkingOptions),[t.linkingOptions]),m=(0,u.useMemo)(()=>j(i).filter(([s])=>s in r),[i,r]);(0,u.useEffect)(()=>{try{let s=new URL(t.linkValue);s.search&&(s.searchParams,l(k(i.map(p=>[p.attribute,s.searchParams.get(p.attribute)??""]))))}catch{l(k(i.map(p=>[p.attribute,""])))}},[t.linkValue,i]);let f=(0,u.useCallback)(s=>p=>{l(q=>{let J={...q,[s]:p};try{let x=new URL(t.linkValue),L=x.searchParams;p.length===0&&L.has(s)?L.delete(s):L.set(s,p),e(x.toString())}catch{}return J})},[t.linkValue,e]);return u.default.createElement(u.default.Fragment,null,m.map(([s,p])=>u.default.createElement(W,{key:s,label:r[s].label,i18nRegistry:t.i18nRegistry,options:p,onChange:f,values:o,collapsed:r[s].collapsed})))};var $=g(y());var z=g(B()),Y=t=>{let{linking:e,...n}=t.options,o=t.neos.globalRegistry.get("frontendConfiguration").get("Prgfx.Neos.LinkEditor").linkEditor?.ignoredOptions??[],l={...e,...k(o.map(i=>[i,!1]))};return $.default.createElement(z.LinkInput,{linkValue:t.value,onLinkChange:t.commit,options:n,linkingOptions:l})};R("Prgfx.Neos.LinkEditor:LinkEditor",{},(t,{frontendConfiguration:e})=>{t.get("containers").set("LinkInput/OptionsPanel/LinkAttributeEditor",M,"end"),e["Prgfx.Neos.LinkEditor"].linkEditor.replace&&t.get("inspector").get("editors").set("Neos.Neos/Inspector/Editors/LinkEditor",{component:Y});let o=t.get("dataLoaders").get("LinkLookup"),l=o.resolveValue,i=(m,f)=>{let s=typeof f=="string"&&/^node:\/\//.test(f)?f.split("?")[0]:f;return l.call(o,m,s)};o.resolveValue=i.bind(o)});})();