Skip to content

Commit 922926f

Browse files
committed
Added matrix element
1 parent 01b63c0 commit 922926f

File tree

19 files changed

+400
-47
lines changed

19 files changed

+400
-47
lines changed

Gemfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ ruby '3.1.3'
99
# github: 'ministryofjustice/fb-metadata-presenter',
1010
# branch: 'accessibility-statement'
1111
# gem 'metadata_presenter', path: '../fb-metadata-presenter'
12-
gem 'metadata_presenter', '3.3.5'
12+
# gem 'metadata_presenter', '3.3.5'
13+
gem 'metadata_presenter', git: 'git@github.com:cabinetoffice/fb-metadata-presenter.git', branch: 'feature/matrix'
1314

1415
gem 'activerecord-session_store'
1516
gem 'administrate'

Gemfile.lock

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
GIT
2+
remote: git@github.com:cabinetoffice/fb-metadata-presenter.git
3+
revision: 9c4ded724434bbd3ac5608058f7d15fcd49ae6d7
4+
branch: feature/matrix
5+
specs:
6+
metadata_presenter (3.4.4)
7+
govspeak (~> 7.1)
8+
govuk_design_system_formbuilder (~> 4.1.1)
9+
json-schema (~> 4.1.1)
10+
kramdown (~> 2.4.0)
11+
rails (~> 7.0.0)
12+
sassc-rails (= 2.1.2)
13+
sprockets
14+
sprockets-rails
15+
uk_postcode
16+
117
GEM
218
remote: https://rubygems.org/
319
specs:
@@ -273,15 +289,6 @@ GEM
273289
net-smtp
274290
marcel (1.0.2)
275291
matrix (0.4.2)
276-
metadata_presenter (3.3.5)
277-
govspeak (~> 7.1)
278-
govuk_design_system_formbuilder (>= 2.1.5)
279-
json-schema (~> 4.1.1)
280-
kramdown (>= 2.4.0)
281-
rails (>= 7.0.0)
282-
sassc-rails (= 2.1.2)
283-
sprockets
284-
sprockets-rails
285292
method_source (1.0.0)
286293
mini_mime (1.1.5)
287294
mini_portile2 (2.8.5)
@@ -699,6 +706,7 @@ GEM
699706
concurrent-ruby (~> 1.0)
700707
tzinfo-data (1.2023.3)
701708
tzinfo (>= 1.0.0)
709+
uk_postcode (2.1.8)
702710
unicode-display_width (2.5.0)
703711
version_gem (1.1.4)
704712
view_component (3.6.0)
@@ -754,7 +762,7 @@ DEPENDENCIES
754762
govuk_design_system_formbuilder
755763
hashie
756764
listen (~> 3.8)
757-
metadata_presenter (= 3.3.5)
765+
metadata_presenter!
758766
omniauth-cognito-idp (= 0.1.1)
759767
omniauth-rails_csrf_protection (~> 1.0.1)
760768
pg (>= 0.18, < 2.0)

app/controllers/application_controller.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,26 @@ def single_page_preview?
144144
end
145145
helper_method :single_page_preview?
146146

147+
# MetadataPresenter expects host apps to provide this hook.
148+
# Editor preview never uses an external start page by default.
149+
def use_external_start_page?
150+
false
151+
end
152+
helper_method :use_external_start_page?
153+
154+
def external_start_page_url
155+
''
156+
end
157+
helper_method :external_start_page_url
158+
159+
def start_page_url
160+
return external_start_page_url if use_external_start_page?
161+
162+
start_page = service.start_page.url.to_s.sub(%r{\A/}, '')
163+
File.join(request.script_name, start_page)
164+
end
165+
helper_method :start_page_url
166+
147167
def service_slug
148168
return service_slug_config if service_slug_config.present?
149169

app/generators/new_component_generator.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ def to_metadata
2222
item['_uuid'] = SecureRandom.uuid
2323
end
2424
end
25+
26+
if matrix_component?(metadata)
27+
metadata['rows'].each { |row| row['id'] = SecureRandom.uuid }
28+
metadata['columns'].each { |column| column['id'] = SecureRandom.uuid }
29+
end
2530
end
2631
end
2732

@@ -47,4 +52,8 @@ def components_of_type(type)
4752
def component_index(component)
4853
component['_id'].split('_').last
4954
end
55+
56+
def matrix_component?(metadata)
57+
metadata['rows'].is_a?(Array) && metadata['columns'].is_a?(Array)
58+
end
5059
end

app/javascript/src/controller_pages.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const DateQuestion = require('./question_date');
3636
const TextQuestion = require('./question_text');
3737
const TextareaQuestion = require('./question_textarea');
3838
const AutocompleteQuestion = require('./question_autocomplete');
39+
const MatrixQuestion = require('./question_matrix');
3940

4041
const Dialog = require('./component_dialog');
4142
const DialogApiRequest = require('./component_dialog_api_request');
@@ -726,6 +727,23 @@ function enhanceQuestions(view) {
726727

727728
});
728729
});
730+
731+
view.$editable.filter("[data-fb-content-type=matrix]").each(function(i, node) {
732+
var modeSwitchWarning = "";
733+
if (view.text.question && view.text.question.matrix) {
734+
modeSwitchWarning = view.text.question.matrix.mode_switch_warning;
735+
}
736+
737+
new MatrixQuestion($(this), {
738+
form: view.saveButton.$form,
739+
text: {
740+
optionalFlag: view.text.question_optional_flag,
741+
matrix: {
742+
modeSwitchWarning: modeSwitchWarning
743+
}
744+
}
745+
});
746+
});
729747
}
730748

731749

app/javascript/src/editable_components.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,7 @@ function editableComponent($node, config) {
10121012
klass = EditableTextareaFieldComponent;
10131013
break;
10141014
case "date":
1015+
case "matrix":
10151016
klass = EditableGroupFieldComponent;
10161017
break;
10171018
case "radios":
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* Matrix Question
3+
* ----------------------------------------------------
4+
* Description:
5+
* Matrix component extension of Question.
6+
*
7+
* Supports editable legend/hint, row labels, column labels,
8+
* and mode switching confirmation in the editor.
9+
**/
10+
11+
const utilities = require('./utilities');
12+
const mergeObjects = utilities.mergeObjects;
13+
const Question = require('./question');
14+
const EditableElement = require('./editable_components').EditableElement;
15+
16+
const SELECTOR_HINT = "fieldset > .govuk-hint";
17+
const SELECTOR_LABEL = "legend > :first-child";
18+
const SELECTOR_ROW_LABEL = "[data-fb-matrix-row-label]";
19+
const SELECTOR_COLUMN_LABEL = "[data-fb-matrix-column-label]";
20+
const SELECTOR_MODE = "[data-fb-matrix-mode]";
21+
const SELECTOR_CELL_INPUTS = "tbody input";
22+
23+
24+
class MatrixQuestion extends Question {
25+
constructor($node, config) {
26+
super($node, mergeObjects({
27+
selectorLabel: SELECTOR_LABEL,
28+
selectorHint: SELECTOR_HINT
29+
}, config));
30+
31+
$node.addClass("MatrixQuestion");
32+
33+
this._rowLabelElements = [];
34+
this._columnLabelElements = [];
35+
36+
this.initialiseMatrixLabelEditing(config);
37+
this.initialiseModeSwitchConfirmation(config);
38+
}
39+
40+
initialiseMatrixLabelEditing(config) {
41+
const question = this;
42+
43+
this.$node.find(SELECTOR_ROW_LABEL).each(function(index) {
44+
const editable = createEditableLabel($(this), config);
45+
question._rowLabelElements.push(editable);
46+
$(this).on("blur.matrix-row-label", function() {
47+
question.syncRowLabel(index, editable.content, $(this).data("fb-matrix-row-id"));
48+
});
49+
});
50+
51+
this.$node.find(SELECTOR_COLUMN_LABEL).each(function(index) {
52+
const editable = createEditableLabel($(this), config);
53+
question._columnLabelElements.push(editable);
54+
$(this).on("blur.matrix-column-label", function() {
55+
question.syncColumnLabel(index, editable.content, $(this).data("fb-matrix-column-id"));
56+
});
57+
});
58+
}
59+
60+
initialiseModeSwitchConfirmation(config) {
61+
const question = this;
62+
const warningText = config.text && config.text.matrix && config.text.matrix.modeSwitchWarning;
63+
64+
this.$node.find(SELECTOR_MODE).on("change.matrix-mode", function() {
65+
const $field = $(this);
66+
const previousValue = question.data.mode || $field.data("fb-matrix-mode-original") || $field.val();
67+
const nextValue = $field.val();
68+
69+
if (previousValue === nextValue) {
70+
return;
71+
}
72+
73+
if (question.hasMatrixCellInput() && warningText && !window.confirm(warningText)) {
74+
$field.val(previousValue);
75+
return;
76+
}
77+
78+
question.data.mode = nextValue;
79+
$field.data("fb-matrix-mode-original", nextValue);
80+
question.editable.emitSaveRequired();
81+
});
82+
}
83+
84+
hasMatrixCellInput() {
85+
let hasInput = false;
86+
87+
this.$node.find(SELECTOR_CELL_INPUTS).each(function() {
88+
const $input = $(this);
89+
const type = ($input.attr("type") || "").toLowerCase();
90+
91+
if ((type === "radio" || type === "checkbox") && $input.prop("checked")) {
92+
hasInput = true;
93+
return false;
94+
}
95+
96+
if (type === "number" && $input.val() !== "") {
97+
hasInput = true;
98+
return false;
99+
}
100+
101+
return true;
102+
});
103+
104+
return hasInput;
105+
}
106+
107+
syncRowLabel(index, label, rowId) {
108+
const row = findAxisItem(this.data.rows, rowId, index);
109+
if (!row) {
110+
return;
111+
}
112+
113+
row.label = label;
114+
this.editable.emitSaveRequired();
115+
}
116+
117+
syncColumnLabel(index, label, columnId) {
118+
const column = findAxisItem(this.data.columns, columnId, index);
119+
if (!column) {
120+
return;
121+
}
122+
123+
column.label = label;
124+
this.editable.emitSaveRequired();
125+
}
126+
}
127+
128+
129+
function createEditableLabel($node, config) {
130+
const existing = $node.data("instance");
131+
if (existing) {
132+
return existing;
133+
}
134+
135+
return new EditableElement($node, config);
136+
}
137+
138+
function findAxisItem(collection, id, index) {
139+
if (!Array.isArray(collection)) {
140+
return null;
141+
}
142+
143+
if (id) {
144+
const match = collection.find(function(item) {
145+
return item.id === id;
146+
});
147+
148+
if (match) {
149+
return match;
150+
}
151+
}
152+
153+
return collection[index] || null;
154+
}
155+
156+
module.exports = MatrixQuestion;

app/views/services/_connection_menu.html.erb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
data-component-type="radios"><span><%= t('components.list.radios') -%></span></li>
2626
<li data-page-type="singlequestion"
2727
data-component-type="checkboxes"><span><%= t('components.list.checkboxes') -%></span></li>
28+
<li data-page-type="singlequestion"
29+
data-component-type="matrix"><span><%= t('components.list.matrix') -%></span></li>
2830
<li data-page-type="singlequestion"
2931
data-component-type="autocomplete"><span><%= t('components.list.autocomplete') -%></span></li>
3032
<li data-page-type="singlequestion"

config/locales/en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ en:
317317
date: 'Date'
318318
radios: 'Radio buttons'
319319
checkboxes: 'Checkboxes'
320+
matrix: 'Matrix'
320321
upload: 'File upload'
321322
multi_upload: 'Multiple File upload'
322323
autocomplete: 'Autocomplete'
@@ -339,6 +340,8 @@ en:
339340
heading: Show this component...
340341
question:
341342
optional_flag: '(optional)'
343+
matrix:
344+
mode_switch_warning: 'Switching matrix mode will clear existing matrix answers. Continue?'
342345
menu:
343346
activator: 'Properties'
344347
remove: 'Delete...'

spec/fixtures/kubernetes_configuration/deployment.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ spec:
9595
envFrom:
9696
- configMapRef:
9797
name: fb-acceptance-tests-date-config-map
98-
image: 860619597616.dkr.ecr.eu-west-2.amazonaws.com/fb-runner:a86508da974d9ede24c9a932ec20537b209b36b1
98+
image: ghcr.io/cabinetoffice/fb-runner:branch-feature-deploy-32d4849
9999
imagePullPolicy: Always
100100
ports:
101101
- containerPort: 3000

0 commit comments

Comments
 (0)