From e347532af40986d50f193417a81d47d680c23258 Mon Sep 17 00:00:00 2001
From: Mark Johnson <mark.johnson@catalyst-eu.net>
Date: Wed, 4 Dec 2024 11:21:01 +0000
Subject: [PATCH 1/2] MDL-83883 datafilter: Fix initial state of binary filter

When a filter is added to a page after the initial page load, no
initialValues are passed, so we need to set a default value.
---
 lib/amd/build/datafilter/filtertypes/binary.min.js     | 2 +-
 lib/amd/build/datafilter/filtertypes/binary.min.js.map | 2 +-
 lib/amd/src/datafilter/filtertypes/binary.js           | 3 ++-
 3 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/lib/amd/build/datafilter/filtertypes/binary.min.js b/lib/amd/build/datafilter/filtertypes/binary.min.js
index 8b5f9d330ce..db5298afcef 100644
--- a/lib/amd/build/datafilter/filtertypes/binary.min.js
+++ b/lib/amd/build/datafilter/filtertypes/binary.min.js
@@ -1,3 +1,3 @@
-define("core/datafilter/filtertypes/binary",["exports","core/datafilter/filtertype","core/datafilter/selectors","core/templates","core/str"],(function(_exports,_filtertype,_selectors,_templates,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=_interopRequireDefault(_filtertype),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates);class _default extends _filtertype.default{constructor(){super(...arguments),_defineProperty(this,"optionOne",void 0),_defineProperty(this,"optionTwo",void 0)}async addValueSelector(initialValues){return[this.optionOne,this.optionTwo]=await this.getTextValues(),this.displayBinarySelection(initialValues[0])}getTextValues(){return(0,_str.get_strings)([{key:"no"},{key:"yes"}])}async displayBinarySelection(){let initialValue=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;const specificFilterSet=this.rootNode.querySelector(_selectors.default.filter.byName(this.filterType)),sourceDataNode=this.getSourceDataForFilter(),context={filtertype:this.filterType,title:sourceDataNode.getAttribute("data-field-title"),required:sourceDataNode.dataset.required,options:[{text:this.optionOne,value:0,selected:0===initialValue},{text:this.optionTwo,value:1,selected:1===initialValue}]};return _templates.default.render("core/datafilter/filtertypes/binary_selector",context).then(((binaryUi,js)=>_templates.default.replaceNodeContents(specificFilterSet.querySelector(_selectors.default.filter.regions.values),binaryUi,js)))}get values(){return[parseInt(this.filterRoot.querySelector('[data-filterfield="'.concat(this.name,'"]')).value)]}}return _exports.default=_default,_exports.default}));
+define("core/datafilter/filtertypes/binary",["exports","core/datafilter/filtertype","core/datafilter/selectors","core/templates","core/str"],(function(_exports,_filtertype,_selectors,_templates,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=_interopRequireDefault(_filtertype),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates);class _default extends _filtertype.default{constructor(){super(...arguments),_defineProperty(this,"optionOne",void 0),_defineProperty(this,"optionTwo",void 0)}async addValueSelector(initialValues){[this.optionOne,this.optionTwo]=await this.getTextValues();const initialValue=initialValues?initialValues[0]:0;return this.displayBinarySelection(initialValue)}getTextValues(){return(0,_str.get_strings)([{key:"no"},{key:"yes"}])}async displayBinarySelection(){let initialValue=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;const specificFilterSet=this.rootNode.querySelector(_selectors.default.filter.byName(this.filterType)),sourceDataNode=this.getSourceDataForFilter(),context={filtertype:this.filterType,title:sourceDataNode.getAttribute("data-field-title"),required:sourceDataNode.dataset.required,options:[{text:this.optionOne,value:0,selected:0===initialValue},{text:this.optionTwo,value:1,selected:1===initialValue}]};return _templates.default.render("core/datafilter/filtertypes/binary_selector",context).then(((binaryUi,js)=>_templates.default.replaceNodeContents(specificFilterSet.querySelector(_selectors.default.filter.regions.values),binaryUi,js)))}get values(){return[parseInt(this.filterRoot.querySelector('[data-filterfield="'.concat(this.name,'"]')).value)]}}return _exports.default=_default,_exports.default}));
 
 //# sourceMappingURL=binary.min.js.map
\ No newline at end of file
diff --git a/lib/amd/build/datafilter/filtertypes/binary.min.js.map b/lib/amd/build/datafilter/filtertypes/binary.min.js.map
index 45a15087d69..cf1292a1e9d 100644
--- a/lib/amd/build/datafilter/filtertypes/binary.min.js.map
+++ b/lib/amd/build/datafilter/filtertypes/binary.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"binary.min.js","sources":["../../../src/datafilter/filtertypes/binary.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Base filter for binary selector ie: (Yes / No).\n *\n * @module     core/datafilter/filtertypes/binary\n * @author     2022 Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>\n * @copyright  2022 Catalyst IT Australia Pty Ltd\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Filter from 'core/datafilter/filtertype';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport {get_strings as getStrings} from 'core/str';\n\nexport default class extends Filter {\n\n    /**\n     * Text string for the first binary option.\n     *\n     * This option (and {@see optionTwo}) are set by {@see getTextValues()}. The base class will set default values,\n     * a subclass can override the method to define its own option.\n     *\n     * @type {String}\n     */\n    optionOne;\n\n    /**\n     * Text string for the second binary option.\n     *\n     * @type {String}\n     */\n    optionTwo;\n\n    /**\n     * Add the value selector to the filter row.\n     *\n     * @param {Array} initialValues The default value for the filter.\n     */\n    async addValueSelector(initialValues) {\n        [this.optionOne, this.optionTwo] = await this.getTextValues();\n        return this.displayBinarySelection(initialValues[0]);\n    }\n\n    /**\n     * Fetch text values for select options.\n     *\n     * Subclasses should override this method to set their own options.\n     *\n     * @returns {Promise}\n     */\n    getTextValues() {\n        return getStrings([{key: 'no'}, {key: 'yes'}]);\n    }\n\n    /**\n     * Renders yes/no select input with proper selection.\n     *\n     * @param {Number} initialValue The default value for the filter.\n     */\n    async displayBinarySelection(initialValue = 0) {\n        // We specify a specific filterset in case there are multiple filtering condition - avoiding glitches.\n        const specificFilterSet = this.rootNode.querySelector(Selectors.filter.byName(this.filterType));\n        const sourceDataNode = this.getSourceDataForFilter();\n        const context = {\n            filtertype: this.filterType,\n            title: sourceDataNode.getAttribute('data-field-title'),\n            required: sourceDataNode.dataset.required,\n            options: [\n                {\n                    text: this.optionOne,\n                    value: 0,\n                    selected: initialValue === 0,\n                },\n                {\n                    text: this.optionTwo,\n                    value: 1,\n                    selected: initialValue === 1,\n                },\n            ]\n        };\n        return Templates.render('core/datafilter/filtertypes/binary_selector', context)\n        .then((binaryUi, js) => {\n            return Templates.replaceNodeContents(specificFilterSet.querySelector(Selectors.filter.regions.values), binaryUi, js);\n        });\n    }\n\n    /**\n     * Get the list of raw values for this filter type.\n     *\n     * @returns {Array}\n     */\n    get values() {\n        return [parseInt(this.filterRoot.querySelector(`[data-filterfield=\"${this.name}\"]`).value)];\n    }\n\n}\n"],"names":["Filter","initialValues","this","optionOne","optionTwo","getTextValues","displayBinarySelection","key","initialValue","specificFilterSet","rootNode","querySelector","Selectors","filter","byName","filterType","sourceDataNode","getSourceDataForFilter","context","filtertype","title","getAttribute","required","dataset","options","text","value","selected","Templates","render","then","binaryUi","js","replaceNodeContents","regions","values","parseInt","filterRoot","name"],"mappings":"irBA6B6BA,+JAwBFC,sBAClBC,KAAKC,UAAWD,KAAKE,iBAAmBF,KAAKG,gBACvCH,KAAKI,uBAAuBL,cAAc,IAUrDI,uBACW,oBAAW,CAAC,CAACE,IAAK,MAAO,CAACA,IAAK,4CAQbC,oEAAe,QAElCC,kBAAoBP,KAAKQ,SAASC,cAAcC,mBAAUC,OAAOC,OAAOZ,KAAKa,aAC7EC,eAAiBd,KAAKe,yBACtBC,QAAU,CACZC,WAAYjB,KAAKa,WACjBK,MAAOJ,eAAeK,aAAa,oBACnCC,SAAUN,eAAeO,QAAQD,SACjCE,QAAS,CACL,CACIC,KAAMvB,KAAKC,UACXuB,MAAO,EACPC,SAA2B,IAAjBnB,cAEd,CACIiB,KAAMvB,KAAKE,UACXsB,MAAO,EACPC,SAA2B,IAAjBnB,uBAIfoB,mBAAUC,OAAO,8CAA+CX,SACtEY,MAAK,CAACC,SAAUC,KACNJ,mBAAUK,oBAAoBxB,kBAAkBE,cAAcC,mBAAUC,OAAOqB,QAAQC,QAASJ,SAAUC,MASrHG,mBACO,CAACC,SAASlC,KAAKmC,WAAW1B,2CAAoCT,KAAKoC,YAAUZ"}
\ No newline at end of file
+{"version":3,"file":"binary.min.js","sources":["../../../src/datafilter/filtertypes/binary.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Base filter for binary selector ie: (Yes / No).\n *\n * @module     core/datafilter/filtertypes/binary\n * @author     2022 Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>\n * @copyright  2022 Catalyst IT Australia Pty Ltd\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Filter from 'core/datafilter/filtertype';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport {get_strings as getStrings} from 'core/str';\n\nexport default class extends Filter {\n\n    /**\n     * Text string for the first binary option.\n     *\n     * This option (and {@see optionTwo}) are set by {@see getTextValues()}. The base class will set default values,\n     * a subclass can override the method to define its own option.\n     *\n     * @type {String}\n     */\n    optionOne;\n\n    /**\n     * Text string for the second binary option.\n     *\n     * @type {String}\n     */\n    optionTwo;\n\n    /**\n     * Add the value selector to the filter row.\n     *\n     * @param {Array} initialValues The default value for the filter.\n     */\n    async addValueSelector(initialValues) {\n        [this.optionOne, this.optionTwo] = await this.getTextValues();\n        const initialValue = initialValues ? initialValues[0] : 0;\n        return this.displayBinarySelection(initialValue);\n    }\n\n    /**\n     * Fetch text values for select options.\n     *\n     * Subclasses should override this method to set their own options.\n     *\n     * @returns {Promise}\n     */\n    getTextValues() {\n        return getStrings([{key: 'no'}, {key: 'yes'}]);\n    }\n\n    /**\n     * Renders yes/no select input with proper selection.\n     *\n     * @param {Number} initialValue The default value for the filter.\n     */\n    async displayBinarySelection(initialValue = 0) {\n        // We specify a specific filterset in case there are multiple filtering condition - avoiding glitches.\n        const specificFilterSet = this.rootNode.querySelector(Selectors.filter.byName(this.filterType));\n        const sourceDataNode = this.getSourceDataForFilter();\n        const context = {\n            filtertype: this.filterType,\n            title: sourceDataNode.getAttribute('data-field-title'),\n            required: sourceDataNode.dataset.required,\n            options: [\n                {\n                    text: this.optionOne,\n                    value: 0,\n                    selected: initialValue === 0,\n                },\n                {\n                    text: this.optionTwo,\n                    value: 1,\n                    selected: initialValue === 1,\n                },\n            ]\n        };\n        return Templates.render('core/datafilter/filtertypes/binary_selector', context)\n        .then((binaryUi, js) => {\n            return Templates.replaceNodeContents(specificFilterSet.querySelector(Selectors.filter.regions.values), binaryUi, js);\n        });\n    }\n\n    /**\n     * Get the list of raw values for this filter type.\n     *\n     * @returns {Array}\n     */\n    get values() {\n        return [parseInt(this.filterRoot.querySelector(`[data-filterfield=\"${this.name}\"]`).value)];\n    }\n\n}\n"],"names":["Filter","initialValues","this","optionOne","optionTwo","getTextValues","initialValue","displayBinarySelection","key","specificFilterSet","rootNode","querySelector","Selectors","filter","byName","filterType","sourceDataNode","getSourceDataForFilter","context","filtertype","title","getAttribute","required","dataset","options","text","value","selected","Templates","render","then","binaryUi","js","replaceNodeContents","regions","values","parseInt","filterRoot","name"],"mappings":"irBA6B6BA,+JAwBFC,gBAClBC,KAAKC,UAAWD,KAAKE,iBAAmBF,KAAKG,sBACxCC,aAAeL,cAAgBA,cAAc,GAAK,SACjDC,KAAKK,uBAAuBD,cAUvCD,uBACW,oBAAW,CAAC,CAACG,IAAK,MAAO,CAACA,IAAK,4CAQbF,oEAAe,QAElCG,kBAAoBP,KAAKQ,SAASC,cAAcC,mBAAUC,OAAOC,OAAOZ,KAAKa,aAC7EC,eAAiBd,KAAKe,yBACtBC,QAAU,CACZC,WAAYjB,KAAKa,WACjBK,MAAOJ,eAAeK,aAAa,oBACnCC,SAAUN,eAAeO,QAAQD,SACjCE,QAAS,CACL,CACIC,KAAMvB,KAAKC,UACXuB,MAAO,EACPC,SAA2B,IAAjBrB,cAEd,CACImB,KAAMvB,KAAKE,UACXsB,MAAO,EACPC,SAA2B,IAAjBrB,uBAIfsB,mBAAUC,OAAO,8CAA+CX,SACtEY,MAAK,CAACC,SAAUC,KACNJ,mBAAUK,oBAAoBxB,kBAAkBE,cAAcC,mBAAUC,OAAOqB,QAAQC,QAASJ,SAAUC,MASrHG,mBACO,CAACC,SAASlC,KAAKmC,WAAW1B,2CAAoCT,KAAKoC,YAAUZ"}
\ No newline at end of file
diff --git a/lib/amd/src/datafilter/filtertypes/binary.js b/lib/amd/src/datafilter/filtertypes/binary.js
index 732c9729ae3..20228641e33 100644
--- a/lib/amd/src/datafilter/filtertypes/binary.js
+++ b/lib/amd/src/datafilter/filtertypes/binary.js
@@ -53,7 +53,8 @@ export default class extends Filter {
      */
     async addValueSelector(initialValues) {
         [this.optionOne, this.optionTwo] = await this.getTextValues();
-        return this.displayBinarySelection(initialValues[0]);
+        const initialValue = initialValues ? initialValues[0] : 0;
+        return this.displayBinarySelection(initialValue);
     }
 
     /**

From bfb057001b1d2075500c3733ec4b2756fd8c99c1 Mon Sep 17 00:00:00 2001
From: Mark Johnson <mark.johnson@catalyst-eu.net>
Date: Wed, 4 Dec 2024 13:18:30 +0000
Subject: [PATCH 2/2] MDL-83883 qbank_editquestion: Add status filter condition

This allows users to filter questions based on whether a question is in
"draft" or "ready" status.
---
 .../datafilter/filtertypes/status.min.js      | 11 +++
 .../datafilter/filtertypes/status.min.js.map  |  1 +
 .../amd/src/datafilter/filtertypes/status.js  | 40 +++++++++
 .../editquestion/classes/plugin_feature.php   |  6 ++
 .../editquestion/classes/status_condition.php | 88 +++++++++++++++++++
 .../lang/en/qbank_editquestion.php            |  3 +
 .../filter_condition_question_status.feature  | 41 +++++++++
 7 files changed, 190 insertions(+)
 create mode 100644 question/bank/editquestion/amd/build/datafilter/filtertypes/status.min.js
 create mode 100644 question/bank/editquestion/amd/build/datafilter/filtertypes/status.min.js.map
 create mode 100644 question/bank/editquestion/amd/src/datafilter/filtertypes/status.js
 create mode 100644 question/bank/editquestion/classes/status_condition.php
 create mode 100644 question/bank/editquestion/tests/behat/filter_condition_question_status.feature

diff --git a/question/bank/editquestion/amd/build/datafilter/filtertypes/status.min.js b/question/bank/editquestion/amd/build/datafilter/filtertypes/status.min.js
new file mode 100644
index 00000000000..df64bdb5965
--- /dev/null
+++ b/question/bank/editquestion/amd/build/datafilter/filtertypes/status.min.js
@@ -0,0 +1,11 @@
+define("qbank_editquestion/datafilter/filtertypes/status",["exports","core/datafilter/filtertypes/binary","core/str"],(function(_exports,_binary,_str){var obj;
+/**
+   * Filter for Status - Ready/Draft
+   *
+   * @module     qbank_editquestion/datafilter/filtertypes/status
+   * @author     Mark Johnson <mark.johnson@catalyst-eu.net>
+   * @copyright  2024 Catalyst IT Europe Ltd
+   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+   */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_binary=(obj=_binary)&&obj.__esModule?obj:{default:obj};class _default extends _binary.default{getTextValues(){return(0,_str.get_strings)([{key:"questionstatusready",component:"qbank_editquestion"},{key:"questionstatusdraft",component:"qbank_editquestion"}])}}return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=status.min.js.map
\ No newline at end of file
diff --git a/question/bank/editquestion/amd/build/datafilter/filtertypes/status.min.js.map b/question/bank/editquestion/amd/build/datafilter/filtertypes/status.min.js.map
new file mode 100644
index 00000000000..32a09371d8e
--- /dev/null
+++ b/question/bank/editquestion/amd/build/datafilter/filtertypes/status.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"status.min.js","sources":["../../../src/datafilter/filtertypes/status.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Filter for Status - Ready/Draft\n *\n * @module     qbank_editquestion/datafilter/filtertypes/status\n * @author     Mark Johnson <mark.johnson@catalyst-eu.net>\n * @copyright  2024 Catalyst IT Europe Ltd\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Binary from 'core/datafilter/filtertypes/binary';\nimport {get_strings as getStrings} from 'core/str';\n\nexport default class extends Binary {\n    /**\n     * Return strings for Draft/Ready statuses.\n     *\n     * @returns {Promise}\n     */\n    getTextValues() {\n        return getStrings([\n            {key: 'questionstatusready', component: 'qbank_editquestion'},\n            {key: 'questionstatusdraft', component: 'qbank_editquestion'}\n        ]);\n    }\n}\n"],"names":["Binary","getTextValues","key","component"],"mappings":";;;;;;;;oKA2B6BA,gBAMzBC,uBACW,oBAAW,CACd,CAACC,IAAK,sBAAuBC,UAAW,sBACxC,CAACD,IAAK,sBAAuBC,UAAW"}
\ No newline at end of file
diff --git a/question/bank/editquestion/amd/src/datafilter/filtertypes/status.js b/question/bank/editquestion/amd/src/datafilter/filtertypes/status.js
new file mode 100644
index 00000000000..5bd0a5f536c
--- /dev/null
+++ b/question/bank/editquestion/amd/src/datafilter/filtertypes/status.js
@@ -0,0 +1,40 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Filter for Status - Ready/Draft
+ *
+ * @module     qbank_editquestion/datafilter/filtertypes/status
+ * @author     Mark Johnson <mark.johnson@catalyst-eu.net>
+ * @copyright  2024 Catalyst IT Europe Ltd
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import Binary from 'core/datafilter/filtertypes/binary';
+import {get_strings as getStrings} from 'core/str';
+
+export default class extends Binary {
+    /**
+     * Return strings for Draft/Ready statuses.
+     *
+     * @returns {Promise}
+     */
+    getTextValues() {
+        return getStrings([
+            {key: 'questionstatusready', component: 'qbank_editquestion'},
+            {key: 'questionstatusdraft', component: 'qbank_editquestion'}
+        ]);
+    }
+}
diff --git a/question/bank/editquestion/classes/plugin_feature.php b/question/bank/editquestion/classes/plugin_feature.php
index bb5b813c28f..9d43930181f 100644
--- a/question/bank/editquestion/classes/plugin_feature.php
+++ b/question/bank/editquestion/classes/plugin_feature.php
@@ -71,4 +71,10 @@ class plugin_feature extends \core_question\local\bank\plugin_features_base {
         ];
     }
 
+    #[\Override]
+    public function get_question_filters(?view $qbank = null): array {
+        return [
+            new status_condition($qbank),
+        ];
+    }
 }
diff --git a/question/bank/editquestion/classes/status_condition.php b/question/bank/editquestion/classes/status_condition.php
new file mode 100644
index 00000000000..97a2af824b1
--- /dev/null
+++ b/question/bank/editquestion/classes/status_condition.php
@@ -0,0 +1,88 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace qbank_editquestion;
+
+use core_question\local\bank\condition;
+use core_question\local\bank\question_version_status;
+
+/**
+ * Filter condition for the status column
+ *
+ * @package   qbank_editquestion
+ * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
+ * @author    Mark Johnson <mark.johnson@catalyst-eu.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class status_condition extends condition {
+    #[\Override]
+    public function get_title() {
+        return get_string('filter:status', 'qbank_editquestion');
+    }
+
+    #[\Override]
+    public static function get_condition_key() {
+        return 'status';
+    }
+
+    #[\Override]
+    public function get_filter_class() {
+        return 'qbank_editquestion/datafilter/filtertypes/status';
+    }
+
+    /**
+     * Return a single join type, we don't want a join type selector for this condition.
+     *
+     * @return array
+     */
+    public function get_join_list(): array {
+        return [
+            self::JOINTYPE_DEFAULT,
+        ];
+    }
+
+    /**
+     * Return an array mapping the values returned from the filter to the values required for the query.
+     *
+     * @return array
+     */
+    protected static function get_status_list() {
+        return [
+            0 => question_version_status::QUESTION_STATUS_READY,
+            1 => question_version_status::QUESTION_STATUS_DRAFT,
+        ];
+    }
+
+    /**
+     * Return an SQL condition to filter qv.status on the selected status.
+     *
+     * @param array $filter
+     * @return array
+     */
+    public static function build_query_from_filter(array $filter): array {
+        if (!isset($filter['values'][0])) {
+            return ['', []];
+        }
+        $statuses = self::get_status_list();
+        if (!array_key_exists($filter['values'][0], $statuses)) {
+            throw new \moodle_exception('filter:invalidstatus', 'qbank_editquestion', '', $filter['values'][0]);
+        }
+        return [
+            'qv.status = :status',
+            ['status' => $statuses[$filter['values'][0]]],
+        ];
+    }
+}
diff --git a/question/bank/editquestion/lang/en/qbank_editquestion.php b/question/bank/editquestion/lang/en/qbank_editquestion.php
index 84312d1a378..02bc1ce45ce 100644
--- a/question/bank/editquestion/lang/en/qbank_editquestion.php
+++ b/question/bank/editquestion/lang/en/qbank_editquestion.php
@@ -23,6 +23,9 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['filter:status'] = 'Status of latest version';
+$string['filter:invalidstatus'] = 'Invalid value "{$a}" for status filter.';
+
 $string['pluginname'] = 'Edit questions';
 $string['privacy:metadata'] = 'The Edit questions question bank plugin does not store any personal data.';
 
diff --git a/question/bank/editquestion/tests/behat/filter_condition_question_status.feature b/question/bank/editquestion/tests/behat/filter_condition_question_status.feature
new file mode 100644
index 00000000000..8e96c030760
--- /dev/null
+++ b/question/bank/editquestion/tests/behat/filter_condition_question_status.feature
@@ -0,0 +1,41 @@
+@qbank @qbank_editquestion @javascript
+Feature: Filter questions by status
+  As a teacher
+  In order to quickly find questions in Draft or Ready status
+  I want to filter the list of questions by status
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "activities" exist:
+      | activity   | name    | intro              | course | idnumber |
+      | qbank      | Qbank 1 | Question bank 1    | C1     | qbank1   |
+    And the following "question categories" exist:
+      | contextlevel    | reference | name           |
+      | Activity module | qbank1    | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype     | name            | questiontext               | status |
+      | Test questions   | truefalse | First question  | Answer the first question  | ready  |
+      | Test questions   | numerical | Second question | Answer the second question | draft  |
+      | Test questions   | essay     | Third question  | Answer the third question  | ready  |
+
+  Scenario: Filter by ready status
+    Given I am on the "Qbank 1" "core_question > question bank" page logged in as "admin"
+    And I should see "First question"
+    And I should see "Second question"
+    And I should see "Third question"
+    When I apply question bank filter "Status of latest version" with value "Ready"
+    Then I should see "First question"
+    And I should not see "Second question"
+    And I should see "Third question"
+
+  Scenario: Filter by draft status
+    Given I am on the "Qbank 1" "core_question > question bank" page logged in as "admin"
+    And I should see "First question"
+    And I should see "Second question"
+    And I should see "Third question"
+    When I apply question bank filter "Status of latest version" with value "Draft"
+    Then I should not see "First question"
+    And I should see "Second question"
+    And I should not see "Third question"