From 527c5b22242264b84bcd9bc038217d2b92737932 Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Tue, 7 Jun 2022 13:00:12 +1200 Subject: [PATCH] FIX Selected tag is not shown when isMutliple is false (#201) * doc: add note about `setTitleField` (Fixes #153) * NEW Add support for saving tag value into has_one components FIX Value not shown if isMultiple is false (#195) * fix: don't default to ID column as pgsql throws an error if comparing string value --- .editorconfig | 1 + client/dist/js/bundle.js | 2 +- client/src/components/TagField.js | 33 ++++-- docs/en/using.md | 14 +++ readme.md | 73 ++++++++---- src/StringTagField.php | 43 +++++++ src/TagField.php | 174 ++++++++++++++++++---------- tests/StringTagFieldTest.php | 18 +++ tests/Stub/TagFieldTestBlogPost.php | 4 + tests/TagFieldTest.php | 35 +++++- yarn.lock | 27 +---- 11 files changed, 300 insertions(+), 124 deletions(-) diff --git a/.editorconfig b/.editorconfig index 7d23593..a054bf2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,7 @@ trim_trailing_whitespace = true [{*.yml,package.json,*.js,*.scss}] indent_size = 2 indent_style = space +quote_type = single # The indent size used in the package.json file cannot be changed: # https://github.com/npm/npm/pull/3180#issuecomment-16336516 diff --git a/client/dist/js/bundle.js b/client/dist/js/bundle.js index cd6a97f..1cde4c7 100644 --- a/client/dist/js/bundle.js +++ b/client/dist/js/bundle.js @@ -1 +1 @@ -!function(e){function t(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s="./client/src/bundles/bundle.js")}({"./client/src/boot/index.js":function(e,t,n){"use strict";var o=n("./client/src/boot/registerComponents.js"),r=function(e){return e&&e.__esModule?e:{default:e}}(o);window.document.addEventListener("DOMContentLoaded",function(){(0,r.default)()})},"./client/src/boot/registerComponents.js":function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(0),s=o(r),i=n("./client/src/components/TagField.js"),a=o(i);t.default=function(){s.default.component.registerMany({TagField:a.default})}},"./client/src/bundles/bundle.js":function(e,t,n){"use strict";n("./client/src/legacy/entwine/TagField.js"),n("./client/src/boot/index.js")},"./client/src/components/TagField.js":function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n={};for(var o in e)t.indexOf(o)>=0||Object.prototype.hasOwnProperty.call(e,o)&&(n[o]=e[o]);return n}function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function a(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function u(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0}),t.Component=void 0;var l=Object.assign||function(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:0,s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=void 0,a=void 0,u=void 0,l=[];return function(){var h=o(n),c=(new Date).getTime(),f=!i||c-i>h;i=c;for(var p=arguments.length,d=Array(p),m=0;m1&&(o=n[0]+"@",e=n[1]),e=e.replace(A,"."),o+i(e.split("."),t).join(".")}function u(e){for(var t,n,o=[],r=0,s=e.length;r=55296&&t<=56319&&r65535&&(e-=65536,t+=S(e>>>10&1023|55296),e=56320|1023&e),t+=S(e)}).join("")}function h(e){return e-48<10?e-22:e-65<26?e-65:e-97<26?e-97:g}function c(e,t){return e+22+75*(e<26)-((0!=t)<<5)}function f(e,t,n){var o=0;for(e=n?k(e/x):e>>1,e+=k(e/t);e>R*O>>1;o+=g)e=k(e/R);return k(o+(R+1)*e/(e+w))}function p(e){var t,n,o,r,i,a,u,c,p,d,m=[],v=e.length,y=0,w=C,x=_;for(n=e.lastIndexOf(P),n<0&&(n=0),o=0;o=128&&s("not-basic"),m.push(e.charCodeAt(o));for(r=n>0?n+1:0;r=v&&s("invalid-input"),c=h(e.charCodeAt(r++)),(c>=g||c>k((b-y)/a))&&s("overflow"),y+=c*a,p=u<=x?j:u>=x+O?O:u-x,!(ck(b/d)&&s("overflow"),a*=d;t=m.length+1,x=f(y-i,t,0==i),k(y/t)>b-w&&s("overflow"),w+=k(y/t),y%=t,m.splice(y++,0,w)}return l(m)}function d(e){var t,n,o,r,i,a,l,h,p,d,m,v,y,w,x,T=[];for(e=u(e),v=e.length,t=C,n=0,i=_,a=0;a=t&&mk((b-n)/y)&&s("overflow"),n+=(l-t)*y,t=l,a=0;ab&&s("overflow"),m==t){for(h=n,p=g;d=p<=i?j:p>=i+O?O:p-i,!(h= 0x80 (not a basic code point)","invalid-input":"Invalid input"},R=g-j,k=Math.floor,S=String.fromCharCode;y={version:"1.4.1",ucs2:{decode:u,encode:l},decode:p,encode:d,toASCII:v,toUnicode:m},void 0!==(r=function(){return y}.call(t,n,t,e))&&(e.exports=r)}()}).call(t,n("./node_modules/webpack/buildin/module.js")(e),n("./node_modules/webpack/buildin/global.js"))},"./node_modules/prop-types/factoryWithThrowingShims.js":function(e,t,n){"use strict";function o(){}function r(){}var s=n("./node_modules/prop-types/lib/ReactPropTypesSecret.js");r.resetWarningCache=o,e.exports=function(){function e(e,t,n,o,r,i){if(i!==s){var a=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw a.name="Invariant Violation",a}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:r,resetWarningCache:o};return n.PropTypes=n,n}},"./node_modules/prop-types/index.js":function(e,t,n){e.exports=n("./node_modules/prop-types/factoryWithThrowingShims.js")()},"./node_modules/prop-types/lib/ReactPropTypesSecret.js":function(e,t,n){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},"./node_modules/querystring-es3/decode.js":function(e,t,n){"use strict";function o(e,t){return Object.prototype.hasOwnProperty.call(e,t)}e.exports=function(e,t,n,s){t=t||"&",n=n||"=";var i={};if("string"!=typeof e||0===e.length)return i;var a=/\+/g;e=e.split(t);var u=1e3;s&&"number"==typeof s.maxKeys&&(u=s.maxKeys);var l=e.length;u>0&&l>u&&(l=u);for(var h=0;h=0?(c=m.substr(0,v),f=m.substr(v+1)):(c=m,f=""),p=decodeURIComponent(c),d=decodeURIComponent(f),o(i,p)?r(i[p])?i[p].push(d):i[p]=[i[p],d]:i[p]=d}return i};var r=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)}},"./node_modules/querystring-es3/encode.js":function(e,t,n){"use strict";function o(e,t){if(e.map)return e.map(t);for(var n=[],o=0;o",'"',"`"," ","\r","\n","\t"],d=["{","}","|","\\","^","`"].concat(p),m=["'"].concat(d),v=["%","/","?",";","#"].concat(m),y=["/","?","#"],b=/^[+a-z0-9A-Z_-]{0,63}$/,g=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,j={javascript:!0,"javascript:":!0},O={javascript:!0,"javascript:":!0},w={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},x=n("./node_modules/querystring-es3/index.js");o.prototype.parse=function(e,t,n){if(!l.isString(e))throw new TypeError("Parameter 'url' must be a string, not "+typeof e);var o=e.indexOf("?"),r=-1!==o&&o127?U+="x":U+=S[E];if(!U.match(b)){var L=R.slice(0,P),M=R.slice(P+1),N=S.match(g);N&&(L.push(N[1]),M.unshift(N[2])),M.length&&(a="/"+M.join(".")+a),this.hostname=L.join(".");break}}}this.hostname.length>255?this.hostname="":this.hostname=this.hostname.toLowerCase(),I||(this.hostname=u.toASCII(this.hostname));var z=this.port?":"+this.port:"",B=this.hostname||"";this.host=B+z,this.href+=this.host,I&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),"/"!==a[0]&&(a="/"+a))}if(!j[d])for(var P=0,k=m.length;P0)&&n.host.split("@");C&&(n.auth=C.shift(),n.host=n.hostname=C.shift())}return n.search=e.search,n.query=e.query,l.isNull(n.pathname)&&l.isNull(n.search)||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.href=n.format(),n}if(!x.length)return n.pathname=null,n.search?n.path="/"+n.search:n.path=null,n.href=n.format(),n;for(var P=x.slice(-1)[0],T=(n.host||e.host||x.length>1)&&("."===P||".."===P)||""===P,q=0,A=x.length;A>=0;A--)P=x[A],"."===P?x.splice(A,1):".."===P?(x.splice(A,1),q++):q&&(x.splice(A,1),q--);if(!g&&!j)for(;q--;q)x.unshift("..");!g||""===x[0]||x[0]&&"/"===x[0].charAt(0)||x.unshift(""),T&&"/"!==x.join("/").substr(-1)&&x.push("");var I=""===x[0]||x[0]&&"/"===x[0].charAt(0);if(_){n.hostname=n.host=I?"":x.length?x.shift():"";var C=!!(n.host&&n.host.indexOf("@")>0)&&n.host.split("@");C&&(n.auth=C.shift(),n.host=n.hostname=C.shift())}return g=g||n.host&&x.length,g&&!I&&x.unshift(""),x.length?n.pathname=x.join("/"):(n.pathname=null,n.path=null),l.isNull(n.pathname)&&l.isNull(n.search)||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.auth=e.auth||n.auth,n.slashes=n.slashes||e.slashes,n.href=n.format(),n},o.prototype.parseHost=function(){var e=this.host,t=c.exec(e);t&&(t=t[0],":"!==t&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)}},"./node_modules/url/util.js":function(e,t,n){"use strict";e.exports={isString:function(e){return"string"==typeof e},isObject:function(e){return"object"==typeof e&&null!==e},isNull:function(e){return null===e},isNullOrUndefined:function(e){return null==e}}},"./node_modules/webpack/buildin/global.js":function(e,t){var n;n=function(){return this}();try{n=n||Function("return this")()||(0,eval)("this")}catch(e){"object"==typeof window&&(n=window)}e.exports=n},"./node_modules/webpack/buildin/module.js":function(e,t){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}},0:function(e,t){e.exports=Injector},1:function(e,t){e.exports=React},2:function(e,t){e.exports=FieldHolder},3:function(e,t){e.exports=IsomorphicFetch},4:function(e,t){e.exports=ReactDom},5:function(e,t){e.exports=ReactSelect}}); \ No newline at end of file +!function(e){function t(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s="./client/src/bundles/bundle.js")}({"./client/src/boot/index.js":function(e,t,n){"use strict";var o=n("./client/src/boot/registerComponents.js"),r=function(e){return e&&e.__esModule?e:{default:e}}(o);window.document.addEventListener("DOMContentLoaded",function(){(0,r.default)()})},"./client/src/boot/registerComponents.js":function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(0),s=o(r),i=n("./client/src/components/TagField.js"),a=o(i);t.default=function(){s.default.component.registerMany({TagField:a.default})}},"./client/src/bundles/bundle.js":function(e,t,n){"use strict";n("./client/src/legacy/entwine/TagField.js"),n("./client/src/boot/index.js")},"./client/src/components/TagField.js":function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n={};for(var o in e)t.indexOf(o)>=0||Object.prototype.hasOwnProperty.call(e,o)&&(n[o]=e[o]);return n}function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function a(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function u(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0}),t.Component=void 0;var l=Object.assign||function(e){for(var t=1;t0){var u=s.value[Object.keys(s.value)[0]];"object"===(void 0===u?"undefined":h(u))&&(s.value=u)}return p.default.createElement(a,l({},s,{onChange:this.handleChange,onBlur:this.handleOnBlur,inputProps:{className:"no-change-track"}},i))}}]),t}(f.Component);P.propTypes={name:C.default.string.isRequired,labelKey:C.default.string.isRequired,valueKey:C.default.string.isRequired,lazyLoad:C.default.bool,creatable:C.default.bool,multi:C.default.bool,disabled:C.default.bool,options:C.default.arrayOf(C.default.object),optionUrl:C.default.string,value:C.default.any,onChange:C.default.func,onBlur:C.default.func},P.defaultProps={labelKey:"Title",valueKey:"Value",disabled:!1,lazyLoad:!1,creatable:!1,multi:!1},t.Component=P,t.default=(0,g.default)(P)},"./client/src/legacy/entwine/TagField.js":function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}var r=Object.assign||function(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:0,s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=void 0,a=void 0,u=void 0,l=[];return function(){var h=o(n),c=(new Date).getTime(),f=!i||c-i>h;i=c;for(var p=arguments.length,d=Array(p),m=0;m1&&(o=n[0]+"@",e=n[1]),e=e.replace(A,"."),o+i(e.split("."),t).join(".")}function u(e){for(var t,n,o=[],r=0,s=e.length;r=55296&&t<=56319&&r65535&&(e-=65536,t+=R(e>>>10&1023|55296),e=56320|1023&e),t+=R(e)}).join("")}function h(e){return e-48<10?e-22:e-65<26?e-65:e-97<26?e-97:g}function c(e,t){return e+22+75*(e<26)-((0!=t)<<5)}function f(e,t,n){var o=0;for(e=n?I(e/x):e>>1,e+=I(e/t);e>k*O>>1;o+=g)e=I(e/k);return I(o+(k+1)*e/(e+w))}function p(e){var t,n,o,r,i,a,u,c,p,d,m=[],y=e.length,v=0,w=C,x=_;for(n=e.lastIndexOf(P),n<0&&(n=0),o=0;o=128&&s("not-basic"),m.push(e.charCodeAt(o));for(r=n>0?n+1:0;r=y&&s("invalid-input"),c=h(e.charCodeAt(r++)),(c>=g||c>I((b-v)/a))&&s("overflow"),v+=c*a,p=u<=x?j:u>=x+O?O:u-x,!(cI(b/d)&&s("overflow"),a*=d;t=m.length+1,x=f(v-i,t,0==i),I(v/t)>b-w&&s("overflow"),w+=I(v/t),v%=t,m.splice(v++,0,w)}return l(m)}function d(e){var t,n,o,r,i,a,l,h,p,d,m,y,v,w,x,T=[];for(e=u(e),y=e.length,t=C,n=0,i=_,a=0;a=t&&mI((b-n)/v)&&s("overflow"),n+=(l-t)*v,t=l,a=0;ab&&s("overflow"),m==t){for(h=n,p=g;d=p<=i?j:p>=i+O?O:p-i,!(h= 0x80 (not a basic code point)","invalid-input":"Invalid input"},k=g-j,I=Math.floor,R=String.fromCharCode;v={version:"1.4.1",ucs2:{decode:u,encode:l},decode:p,encode:d,toASCII:y,toUnicode:m},void 0!==(r=function(){return v}.call(t,n,t,e))&&(e.exports=r)}()}).call(t,n("./node_modules/webpack/buildin/module.js")(e),n("./node_modules/webpack/buildin/global.js"))},"./node_modules/prop-types/factoryWithThrowingShims.js":function(e,t,n){"use strict";function o(){}function r(){}var s=n("./node_modules/prop-types/lib/ReactPropTypesSecret.js");r.resetWarningCache=o,e.exports=function(){function e(e,t,n,o,r,i){if(i!==s){var a=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw a.name="Invariant Violation",a}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:r,resetWarningCache:o};return n.PropTypes=n,n}},"./node_modules/prop-types/index.js":function(e,t,n){e.exports=n("./node_modules/prop-types/factoryWithThrowingShims.js")()},"./node_modules/prop-types/lib/ReactPropTypesSecret.js":function(e,t,n){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},"./node_modules/querystring-es3/decode.js":function(e,t,n){"use strict";function o(e,t){return Object.prototype.hasOwnProperty.call(e,t)}e.exports=function(e,t,n,s){t=t||"&",n=n||"=";var i={};if("string"!=typeof e||0===e.length)return i;var a=/\+/g;e=e.split(t);var u=1e3;s&&"number"==typeof s.maxKeys&&(u=s.maxKeys);var l=e.length;u>0&&l>u&&(l=u);for(var h=0;h=0?(c=m.substr(0,y),f=m.substr(y+1)):(c=m,f=""),p=decodeURIComponent(c),d=decodeURIComponent(f),o(i,p)?r(i[p])?i[p].push(d):i[p]=[i[p],d]:i[p]=d}return i};var r=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)}},"./node_modules/querystring-es3/encode.js":function(e,t,n){"use strict";function o(e,t){if(e.map)return e.map(t);for(var n=[],o=0;o",'"',"`"," ","\r","\n","\t"],d=["{","}","|","\\","^","`"].concat(p),m=["'"].concat(d),y=["%","/","?",";","#"].concat(m),v=["/","?","#"],b=/^[+a-z0-9A-Z_-]{0,63}$/,g=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,j={javascript:!0,"javascript:":!0},O={javascript:!0,"javascript:":!0},w={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},x=n("./node_modules/querystring-es3/index.js");o.prototype.parse=function(e,t,n){if(!l.isString(e))throw new TypeError("Parameter 'url' must be a string, not "+typeof e);var o=e.indexOf("?"),r=-1!==o&&o127?U+="x":U+=R[E];if(!U.match(b)){var L=k.slice(0,P),M=k.slice(P+1),N=R.match(g);N&&(L.push(N[1]),M.unshift(N[2])),M.length&&(a="/"+M.join(".")+a),this.hostname=L.join(".");break}}}this.hostname.length>255?this.hostname="":this.hostname=this.hostname.toLowerCase(),S||(this.hostname=u.toASCII(this.hostname));var z=this.port?":"+this.port:"",B=this.hostname||"";this.host=B+z,this.href+=this.host,S&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),"/"!==a[0]&&(a="/"+a))}if(!j[d])for(var P=0,I=m.length;P0)&&n.host.split("@");C&&(n.auth=C.shift(),n.host=n.hostname=C.shift())}return n.search=e.search,n.query=e.query,l.isNull(n.pathname)&&l.isNull(n.search)||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.href=n.format(),n}if(!x.length)return n.pathname=null,n.search?n.path="/"+n.search:n.path=null,n.href=n.format(),n;for(var P=x.slice(-1)[0],T=(n.host||e.host||x.length>1)&&("."===P||".."===P)||""===P,q=0,A=x.length;A>=0;A--)P=x[A],"."===P?x.splice(A,1):".."===P?(x.splice(A,1),q++):q&&(x.splice(A,1),q--);if(!g&&!j)for(;q--;q)x.unshift("..");!g||""===x[0]||x[0]&&"/"===x[0].charAt(0)||x.unshift(""),T&&"/"!==x.join("/").substr(-1)&&x.push("");var S=""===x[0]||x[0]&&"/"===x[0].charAt(0);if(_){n.hostname=n.host=S?"":x.length?x.shift():"";var C=!!(n.host&&n.host.indexOf("@")>0)&&n.host.split("@");C&&(n.auth=C.shift(),n.host=n.hostname=C.shift())}return g=g||n.host&&x.length,g&&!S&&x.unshift(""),x.length?n.pathname=x.join("/"):(n.pathname=null,n.path=null),l.isNull(n.pathname)&&l.isNull(n.search)||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.auth=e.auth||n.auth,n.slashes=n.slashes||e.slashes,n.href=n.format(),n},o.prototype.parseHost=function(){var e=this.host,t=c.exec(e);t&&(t=t[0],":"!==t&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)}},"./node_modules/url/util.js":function(e,t,n){"use strict";e.exports={isString:function(e){return"string"==typeof e},isObject:function(e){return"object"==typeof e&&null!==e},isNull:function(e){return null===e},isNullOrUndefined:function(e){return null==e}}},"./node_modules/webpack/buildin/global.js":function(e,t){var n;n=function(){return this}();try{n=n||Function("return this")()||(0,eval)("this")}catch(e){"object"==typeof window&&(n=window)}e.exports=n},"./node_modules/webpack/buildin/module.js":function(e,t){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}},0:function(e,t){e.exports=Injector},1:function(e,t){e.exports=React},2:function(e,t){e.exports=FieldHolder},3:function(e,t){e.exports=IsomorphicFetch},4:function(e,t){e.exports=ReactDom},5:function(e,t){e.exports=ReactSelect}}); \ No newline at end of file diff --git a/client/src/components/TagField.js b/client/src/components/TagField.js index 84893f1..958ccb9 100644 --- a/client/src/components/TagField.js +++ b/client/src/components/TagField.js @@ -56,7 +56,7 @@ class TagField extends Component { } this.setState({ - value + value, }); } @@ -76,9 +76,7 @@ class TagField extends Component { * * @link https://github.com/JedWatson/react-select/issues/805 */ - handleOnBlur() { - - } + handleOnBlur() {} /** * Initiate a request to fetch options, optionally using the given string as a filter. @@ -94,20 +92,17 @@ class TagField extends Component { return fetch(url.format(fetchURL), { credentials: 'same-origin' }) .then((response) => response.json()) .then((json) => ({ - options: json.items.map(item => ({ + options: json.items.map((item) => ({ [labelKey]: item.Title, [valueKey]: item.Value, - })) + Selected: item.Selected, + })), })); } render() { - const { - lazyLoad, - options, - creatable, - ...passThroughAttributes - } = this.props; + const { lazyLoad, options, creatable, ...passThroughAttributes } = + this.props; const optionAttributes = lazyLoad ? { loadOptions: this.getOptions } @@ -128,6 +123,20 @@ class TagField extends Component { passThroughAttributes.value = this.state.value; } + // if this is a single select then we just need the first value + if (!passThroughAttributes.multi && passThroughAttributes.value) { + if (Object.keys(passThroughAttributes.value).length > 0) { + const value = + passThroughAttributes.value[ + Object.keys(passThroughAttributes.value)[0] + ]; + + if (typeof value === 'object') { + passThroughAttributes.value = value; + } + } + } + return ( setTitleField('Name'); +``` diff --git a/readme.md b/readme.md index 5037b22..fccac8a 100644 --- a/readme.md +++ b/readme.md @@ -39,9 +39,9 @@ use SilverStripe\ORM\DataObject; class BlogPost extends DataObject { - private static $many_many = [ - 'BlogTags' => BlogTag::class - ]; + private static $many_many = [ + 'BlogTags' => BlogTag::class + ]; } ``` @@ -50,27 +50,28 @@ use SilverStripe\ORM\DataObject; class BlogTag extends DataObject { - private static $db = [ - 'Title' => 'Varchar(200)', - ]; + private static $db = [ + 'Title' => 'Varchar(200)', + ]; - private static $belongs_many_many = [ - 'BlogPosts' => BlogPost::class - ]; + private static $belongs_many_many = [ + 'BlogPosts' => BlogPost::class + ]; } ``` ```php $field = TagField::create( - 'BlogTags', - 'Blog Tags', - BlogTag::get(), - $this->BlogTags() + 'BlogTags', + 'Blog Tags', + BlogTag::get(), + $this->BlogTags() ) - ->setShouldLazyLoad(true) // tags should be lazy loaded - ->setCanCreate(true); // new tag DataObjects can be created + ->setShouldLazyLoad(true) // tags should be lazy loaded + ->setCanCreate(true); // new tag DataObjects can be created ``` -**Note:** This assumes you have imported the namespaces class, e.g. use SilverStripe\TagField\TagField; +**Note:** This assumes you have imported the namespaces class, e.g. use +SilverStripe\TagField\TagField; ### String Tags @@ -79,18 +80,18 @@ use SilverStripe\ORM\DataObject; class BlogPost extends DataObject { - private static $db = [ - 'Tags' => 'Text', - ]; + private static $db = [ + 'Tags' => 'Text', + ]; } ``` ```php $field = StringTagField::create( - 'Tags', - 'Tags', + 'Tags', + 'Tags', ['one', 'two'], - explode(',', $this->Tags) + explode(',', $this->Tags) ); $field->setShouldLazyLoad(true); // tags should be lazy loaded @@ -98,12 +99,34 @@ $field->setShouldLazyLoad(true); // tags should be lazy loaded You can find more in-depth documentation in [docs/en](docs/en/introduction.md). +## Using TagField with silverstripe-taxonomy + +TagField assumes a `Title` field on objects. For classes without a `Title` field +use `setTitleField` to modify accordingly. + +```php +$field = TagField::create( + 'Tags', + 'Blog Tags', + TaxonomyTerm::get(), +) + ->setTitleField('Name'); +``` + ## Versioning -This library follows [Semver](http://semver.org). According to Semver, you will be able to upgrade to any minor or patch version of this library without any breaking changes to the public API. Semver also requires that we clearly define the public API for this library. +This library follows [Semver](http://semver.org). According to Semver, you will +be able to upgrade to any minor or patch version of this library without any +breaking changes to the public API. Semver also requires that we clearly define +the public API for this library. -All methods, with `public` visibility, are part of the public API. All other methods are not part of the public API. Where possible, we'll try to keep `protected` methods backwards-compatible in minor/patch versions, but if you're overriding methods then please test your work before upgrading. +All methods, with `public` visibility, are part of the public API. All other +methods are not part of the public API. Where possible, we'll try to keep +`protected` methods backwards-compatible in minor/patch versions, but if you're +overriding methods then please test your work before upgrading. ## Reporting Issues -Please [create an issue](http://github.com/silverstripe/silverstripe-tagfield/issues) for any bugs you've found, or features you're missing. +Please [create an +issue](http://github.com/silverstripe/silverstripe-tagfield/issues) for any bugs +you've found, or features you're missing. diff --git a/src/StringTagField.php b/src/StringTagField.php index 180d7a5..556286f 100644 --- a/src/StringTagField.php +++ b/src/StringTagField.php @@ -315,6 +315,49 @@ class StringTagField extends DropdownField return $response; } + /** + * Get or create tag with the given value + * + * @param string $term + * @return DataObject|bool + */ + protected function getOrCreateTag($term) + { + // Check if existing record can be found + $source = $this->getSourceList(); + if (!$source) { + return false; + } + + $titleField = $this->getTitleField(); + $record = $source + ->filter($titleField, $term) + ->first(); + if ($record) { + return $record; + } + + // Create new instance if not yet saved + if ($this->getCanCreate()) { + $dataClass = $source->dataClass(); + $record = Injector::inst()->create($dataClass); + + if (is_array($term)) { + $term = $term['Value']; + } + + $record->{$titleField} = $term; + $record->write(); + if ($source instanceof SS_List) { + $source->add($record); + } + return $record; + } + + return false; + } + + /** * Returns array of arrays representing tags that partially match the given search term * diff --git a/src/TagField.php b/src/TagField.php index 2c0b2ef..177ab23 100644 --- a/src/TagField.php +++ b/src/TagField.php @@ -13,6 +13,7 @@ use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; +use SilverStripe\ORM\FieldType\DBMultiEnum; use SilverStripe\ORM\Relation; use SilverStripe\ORM\SS_List; use SilverStripe\View\ArrayData; @@ -221,10 +222,16 @@ class TagField extends MultiSelectField public function getSchemaDataDefaults() { $options = $this->getOptions(true); + $name = $this->getName(); + + if ($this->getIsMultiple() && strpos($name, '[') === false) { + $name .= '[]'; + } + $schema = array_merge( parent::getSchemaDataDefaults(), [ - 'name' => $this->getName() . '[]', + 'name' => $name, 'lazyLoad' => $this->getShouldLazyLoad(), 'creatable' => $this->getCanCreate(), 'multi' => $this->getIsMultiple(), @@ -265,21 +272,35 @@ class TagField extends MultiSelectField $dataClass = $source->dataClass(); - $values = $this->Value(); + $values = $this->getValueArray(); // If we have no values and we only want selected options we can bail here if (empty($values) && $onlySelected) { return ArrayList::create(); } + $titleField = $this->getTitleField(); + // Convert an array of values into a datalist of options if (!$values instanceof SS_List) { if (is_array($values) && !empty($values)) { + // if values is an array of Ids then we should look up via + // ID. + if (array_filter($values, 'is_int')) { + $queryField = 'ID'; + } else { + $queryField = $titleField; + } + if (is_a($source, DataList::class)) { - $values = $source->filter($this->getTitleField(), $values); + $values = $source->filterAny([ + $queryField => $values + ]); } else { $values = DataList::create($dataClass) - ->filter($this->getTitleField(), $values); + ->filterAny([ + $queryField => $values + ]); } } else { $values = ArrayList::create(); @@ -287,13 +308,13 @@ class TagField extends MultiSelectField } // Prep a function to parse a dataobject into an option - $addOption = function (DataObject $item) use ($options, $values) { - $titleField = $this->getTitleField(); + $addOption = function (DataObject $item) use ($options, $values, $titleField) { $option = $item->$titleField; + $options->push(ArrayData::create([ 'Title' => $option, 'Value' => $option, - 'Selected' => (bool) $values->find('ID', $item->ID) + 'Selected' => (bool) $values->find($titleField, $option) ])); }; @@ -304,36 +325,10 @@ class TagField extends MultiSelectField } $source->each($addOption); + return $options; } - /** - * {@inheritdoc} - */ - public function setValue($value, $source = null) - { - if ($source instanceof DataObject) { - $name = $this->getName(); - - if ($source->hasMethod($name)) { - $value = $source->$name()->column($this->getTitleField()); - } - } - - if (!is_array($value)) { - return parent::setValue($value); - } - - // Safely map php / react-select values to flat list - $values = []; - foreach ($value as $item) { - if ($item) { - $values[] = isset($item['Value']) ? $item['Value'] : $item; - } - } - - return parent::setValue($values); - } /** * Gets the source array if required @@ -350,6 +345,7 @@ class TagField extends MultiSelectField return $this->source; } + /** * Intercept DataList source * @@ -368,30 +364,55 @@ class TagField extends MultiSelectField return $this; } + /** * @param DataObject|DataObjectInterface $record DataObject to save data into * @throws Exception */ public function getAttributes() { + $name = $this->getName(); + + if ($this->getIsMultiple() && strpos($name, '[') === false) { + $name .= '[]'; + } + return array_merge( parent::getAttributes(), [ - 'name' => $this->getName() . '[]', + 'name' => $name, 'style' => 'width: 100%', 'data-schema' => json_encode($this->getSchemaData()), ] ); } + + protected function getListValues($values): array + { + if (empty($values)) { + return []; + } + + if (is_array($values)) { + return $values; + } + + if ($values instanceof SS_List) { + return $values->column($this->getTitleField()); + } + + return [trim((string) $values)]; + } + + /** * {@inheritdoc} */ public function saveInto(DataObjectInterface $record) { $name = $this->getName(); - $titleField = $this->getTitleField(); - $values = $this->Value(); + $values = $this->getValueArray(); $ids = []; @@ -399,49 +420,70 @@ class TagField extends MultiSelectField $values = []; } - if (empty($record) || empty($titleField)) { + if (empty($record)) { return; } - if (!$record->hasMethod($name)) { - throw new Exception( - sprintf("%s does not have a %s method", get_class($record), $name) - ); - } - /** @var Relation $relation */ - $relation = $record->$name(); + $relation = $record->hasMethod($name) ? $record->$name() : null; foreach ($values as $key => $value) { - // Get or create record - $record = $this->getOrCreateTag($value); - if ($record) { - $ids[] = $record->ID; - $values[$key] = $record->Title; + $tag = $this->getOrCreateTag($value); + + if ($tag) { + $ids[] = $tag->ID; + $values[$key] = $tag->Title; } } - $relation->setByIDList(array_filter($ids ?? [])); + + if ($relation instanceof Relation) { + // Save ids into relation + $relation->setByIDList(array_filter($ids ?? [])); + } elseif ($record->hasField($name)) { + if ($this->getIsMultiple()) { + if ($record->obj($name) instanceof DBMultiEnum) { + // Save dataValue into field... a CSV for DBMultiEnum + $record->$name = $this->csvEncode(array_filter(array_values($values))); + } else { + // ... JSON-encoded string for other fields + $record->$name = $this->stringEncode(array_filter(array_values($values))); + } + } else { + if (isset($tag) && $tag->ID) { + $record->$name = $tag->ID; + } else { + $record->$name = null; + } + } + } } /** * Get or create tag with the given value * - * @param string $term + * @param string $value + * * @return DataObject|bool */ - protected function getOrCreateTag($term) + protected function getOrCreateTag($value) { + if (is_array($value)) { + $value = $value['Value'] ?? ''; + } + // Check if existing record can be found $source = $this->getSourceList(); + $titleField = $this->getTitleField(); + if (!$source) { return false; } - $titleField = $this->getTitleField(); $record = $source - ->filter($titleField, $term) + ->filter($titleField, $value) ->first(); + if ($record) { return $record; } @@ -450,16 +492,13 @@ class TagField extends MultiSelectField if ($this->getCanCreate()) { $dataClass = $source->dataClass(); $record = Injector::inst()->create($dataClass); - - if (is_array($term)) { - $term = $term['Value']; - } - - $record->{$titleField} = $term; + $record->{$titleField} = $value; $record->write(); + if ($source instanceof SS_List) { $source->add($record); } + return $record; } @@ -506,7 +545,8 @@ class TagField extends MultiSelectField // Map into a distinct list $items = []; $titleField = $this->getTitleField(); - foreach ($query->map('ID', $titleField) as $id => $title) { + + foreach ($query->map('ID', $titleField)->values() as $title) { $items[$title] = [ 'Title' => $title, 'Value' => $title, @@ -567,4 +607,14 @@ class TagField extends MultiSelectField return $data; } + + + public function getSchemaDataType(): string + { + if ($this->getIsMultiple()) { + return self::SCHEMA_DATA_TYPE_MULTISELECT; + } + + return self::SCHEMA_DATA_TYPE_SINGLESELECT; + } } diff --git a/tests/StringTagFieldTest.php b/tests/StringTagFieldTest.php index c56e69f..024fd71 100755 --- a/tests/StringTagFieldTest.php +++ b/tests/StringTagFieldTest.php @@ -171,6 +171,24 @@ class StringTagFieldTest extends SapphireTest $this->assertNotEmpty($attributes['data-schema']); } + /** + * Ensure a source of tags matches the given string tag names + * + * @param array $expected + * @param DataList $actualSource + */ + protected function compareTagLists(array $expected, DataList $actualSource) + { + $actual = array_keys($actualSource->map('ID', 'Title')->toArray()); + sort($expected); + sort($actual); + + $this->assertEquals( + $expected, + $actual + ); + } + /** * @param array $parameters * @return HTTPRequest diff --git a/tests/Stub/TagFieldTestBlogPost.php b/tests/Stub/TagFieldTestBlogPost.php index e999297..d6db3dd 100644 --- a/tests/Stub/TagFieldTestBlogPost.php +++ b/tests/Stub/TagFieldTestBlogPost.php @@ -18,4 +18,8 @@ class TagFieldTestBlogPost extends DataObject implements TestOnly private static $many_many = [ 'Tags' => TagFieldTestBlogTag::class ]; + + private static $has_one = [ + 'PrimaryTag' => TagFieldTestBlogTag::class + ]; } diff --git a/tests/TagFieldTest.php b/tests/TagFieldTest.php index 9a70742..4ebfc8d 100755 --- a/tests/TagFieldTest.php +++ b/tests/TagFieldTest.php @@ -36,12 +36,40 @@ class TagFieldTest extends SapphireTest $field->setValue(['Tag3', 'Tag4']); $field->saveInto($record); $record->write(); + $this->compareExpectedAndActualTags( ['Tag3', 'Tag4'], $record ); } + public function testItSavesToHasOne() + { + $record = $this->getNewTagFieldTestBlogPost('BlogPost1'); + $tag = new TagFieldTestBlogTag(); + $tag->Title = 'Foobar'; + $tag->write(); + + $field = new TagField('PrimaryTagID', '', new DataList(TagFieldTestBlogTag::class)); + $field->setIsMultiple(false); + + $field->setValue('Foobar'); + $field->saveInto($record); + $record->write(); + + $this->assertEquals($tag->ID, $record->PrimaryTagID, 'The tag is saved to a has_one'); + + $tag = new TagFieldTestBlogTag(); + $tag->Title = 'Foobarbaz'; + $tag->write(); + + $field->setValue(['Foobarbaz']); + $field->saveInto($record); + $record->write(); + + $this->assertEquals($tag->ID, $record->PrimaryTagID, 'The tag is saved to a has_one'); + } + /** * @param string $name * @@ -127,7 +155,7 @@ class TagFieldTest extends SapphireTest $record ); } - + public function testSavesReactTags() { $record = $this->getNewTagFieldTestBlogPost('BlogPost1'); @@ -307,7 +335,7 @@ class TagFieldTest extends SapphireTest $this->objFromFixture(TagFieldTestBlogPost::class, 'BlogPost2') ); - $ids = TagFieldTestBlogTag::get()->column('Title'); + $ids = TagFieldTestBlogTag::get()->column('ID'); $this->assertEquals($field->Value(), $ids); } @@ -357,7 +385,9 @@ class TagFieldTest extends SapphireTest $tag = TagFieldTestBlogTag::get()->first(); $this->assertNotEmpty($tag); $this->assertEquals('New Tag', $tag->Title); + $record = TagFieldTestBlogPost::get()->byID($record->ID); + $this->assertEquals( $tag->ID, $record->Tags()->first()->ID @@ -386,6 +416,7 @@ class TagFieldTest extends SapphireTest $selectedTag = $source->First(); $unselectedTag = $source->Last(); $value = $source->filter('ID', $selectedTag->ID); // arbitrary subset + $field = new TagField('TestField', null, $source, $value); // Not the cleanest way to assert this, but getOptions() is protected diff --git a/yarn.lock b/yarn.lock index b26867d..4aa9c2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3399,7 +3399,7 @@ debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -5752,7 +5752,7 @@ imports-loader@^0.6.5: loader-utils "0.2.x" source-map "0.1.x" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -7358,11 +7358,6 @@ lodash._basecopy@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" integrity sha1-jaDmqHbPNEwK2KVIghEd08XHyjY= -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -7371,16 +7366,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*, lodash._bindcallback@^3.0.0: +lodash._bindcallback@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= - lodash._createassigner@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" @@ -7390,19 +7380,12 @@ lodash._createassigner@^3.0.0: lodash._isiterateecall "^3.0.0" lodash.restparam "^3.0.0" -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= @@ -7518,7 +7501,7 @@ lodash.pick@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= -lodash.restparam@*, lodash.restparam@^3.0.0: +lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=