diff --git a/.prettierrc.mjs b/.prettierrc.mjs
new file mode 100644
index 000000000..607270d42
--- /dev/null
+++ b/.prettierrc.mjs
@@ -0,0 +1,12 @@
+/**
+ * @type {import('prettier').Config}
+ */
+const config = {
+ braceStyle: "stroustrup",
+ plugins: ["prettier-plugin-brace-style"],
+ printWidth: 120,
+ singleQuote: true,
+ tabWidth: 2,
+};
+
+export default config;
diff --git a/app/assets/javascripts/filters.js b/app/assets/javascripts/filters.js
index d929ffb26..1cc3d21ba 100644
--- a/app/assets/javascripts/filters.js
+++ b/app/assets/javascripts/filters.js
@@ -1,158 +1,169 @@
-$(() => {
- $('.js-filter-select').toArray().forEach(async (el) => {
- const $select = $(el);
- const $form = $select.closest('form');
- const $formFilters = $form.find('.form--filter');
- const $saveButton = $form.find('.filter-save');
- const $isDefaultCheckbox = $form.find('.filter-is-default');
- const categoryId = $isDefaultCheckbox.val()?.toString();
- let defaultFilter = await QPixel.defaultFilter(categoryId);
- const $deleteButton = $form.find('.filter-delete');
-
- // Enables/Disables Save & Delete buttons programatically
- async function computeEnables() {
- const filters = await QPixel.filters();
- const filterName = $select.val()?.toString();
-
- // Nothing set
- if (!filterName) {
- $saveButton.prop('disabled', true);
- $deleteButton.prop('disabled', true);
- return;
- }
+document.addEventListener('DOMContentLoaded', () => {
+ $('.js-filter-select')
+ .toArray()
+ .forEach(async (el) => {
+ const $select = $(el);
+ const $form = $select.closest('form');
+ const $formFilters = $form.find('.form--filter');
+ const $saveButton = $form.find('.filter-save');
+ const $isDefaultCheckbox = $form.find('.filter-is-default');
+ const categoryId = $isDefaultCheckbox.val()?.toString();
+ let defaultFilter = categoryId ? await QPixel.defaultFilter(categoryId) : null;
+ const $deleteButton = $form.find('.filter-delete');
+
+ // Enables/Disables Save & Delete buttons programatically
+ async function computeEnables() {
+ const filters = await QPixel.filters();
+ const filterName = $select.val()?.toString();
+
+ // Nothing set
+ if (!filterName) {
+ $saveButton.prop('disabled', true);
+ $deleteButton.prop('disabled', true);
+ return;
+ }
- const filter = filters[filterName]
+ const filter = filters[filterName];
- // New filter
- if (!filter) {
- $saveButton.prop('disabled', false);
- $deleteButton.prop('disabled', true);
- return;
- }
+ // New filter
+ if (!filter) {
+ $saveButton.prop('disabled', false);
+ $deleteButton.prop('disabled', true);
+ return;
+ }
- // Not a new filter
- $deleteButton.prop('disabled', filter.system);
+ // Not a new filter
+ $deleteButton.prop('disabled', filter.system);
- const hasChanges = [...$formFilters].some((el) => {
- const filterValue = filter[el.dataset.name];
- let elValue = /** @type {string | undefined[]} */ ($(el).val());
- if (filterValue?.constructor == Array) {
- elValue = elValue ?? [];
- return filterValue.length != elValue.length || filterValue.some((v, i) => v[1] != elValue[i]);
- }
- else {
- return filterValue ? filterValue != elValue : elValue;
- }
- });
- const defaultStatusChanged = $isDefaultCheckbox.prop('checked') != (defaultFilter === $select.val());
- $saveButton.prop('disabled', !defaultStatusChanged && (filter.system || !hasChanges));
- }
-
- async function initializeSelect() {
- defaultFilter = await QPixel.defaultFilter(categoryId);
- $isDefaultCheckbox.prop('checked', defaultFilter === $select.val());
- const filters = await QPixel.filters();
-
- function template(option) {
- if (option.id == '') { return 'Default'; }
-
- const filter = filters[option.id];
- const name = `${option.text}`;
- const systemIndicator = filter?.system
- ? ' (System)'
- : '';
- const newIndicator = !filter
- ? ' (New)'
- : '';
- return $(name + systemIndicator + newIndicator);
+ const hasChanges = [...$formFilters].some((el) => {
+ const filterValue = filter[el.dataset.name];
+ let elValue = /** @type {string | undefined[]} */ ($(el).val());
+ if (filterValue?.constructor == Array) {
+ elValue = elValue ?? [];
+ return filterValue.length != elValue.length || filterValue.some((v, i) => v[1] != elValue[i]);
+ }
+ else {
+ return filterValue ? filterValue != elValue : elValue;
+ }
+ });
+ const defaultStatusChanged = $isDefaultCheckbox.prop('checked') != (defaultFilter === $select.val());
+ $saveButton.prop('disabled', !defaultStatusChanged && (filter.system || !hasChanges));
}
- // Clear out any old options
- $select.children().filter((_, /** @type{HTMLOptionElement} */ option) => {
- return option.value && !filters[option.value];
- }).detach();
+ async function initializeSelect() {
+ defaultFilter = categoryId ? await QPixel.defaultFilter(categoryId) : null;
+ $isDefaultCheckbox.prop('checked', defaultFilter === $select.val());
+ const filters = await QPixel.filters();
- $select.select2({
- data: Object.keys(filters).map((filterName) => {
- return {
- id: filterName,
- text: filterName
+ function template(option) {
+ if (option.id == '') {
+ return 'Default';
}
- }),
- tags: true,
- templateResult: template,
- templateSelection: template
- });
- $select.on('select2:select', /** @type {(event: Select2.Event) => void} */ (async (evt) => {
- const filterName = evt.params.data.id;
- const preset = filters[filterName];
+ const filter = filters[option.id];
+ const name = `${option.text}`;
+ const systemIndicator = filter?.system ? ' (System)' : '';
+ const newIndicator = !filter ? ' (New)' : '';
+ return $(name + systemIndicator + newIndicator);
+ }
- $isDefaultCheckbox.prop('checked', defaultFilter === $select.val());
+ // Clear out any old options
+ $select
+ .children()
+ .filter((_, /** @type{HTMLOptionElement} */ option) => {
+ return option.value && !filters[option.value];
+ })
+ .detach();
+
+ $select.select2({
+ data: Object.keys(filters).map((filterName) => {
+ return {
+ id: filterName,
+ text: filterName,
+ };
+ }),
+ tags: true,
+ templateResult: template,
+ templateSelection: template,
+ });
+
+ $select.on(
+ 'select2:select',
+ /** @type {(event: Select2.Event) => void} */ (
+ async (evt) => {
+ const filterName = evt.params.data.id;
+ const preset = filters[filterName];
+
+ $isDefaultCheckbox.prop('checked', defaultFilter === $select.val());
+ computeEnables();
+
+ // Name is not one of the presets, i.e user is creating a new preset
+ if (!preset) {
+ return;
+ }
+
+ for (const [name, value] of Object.entries(preset)) {
+ const $el = $form.find(`.form--filter[data-name=${name}]`);
+ if (value?.constructor == Array) {
+ $el.val(null);
+ for (const val of value) {
+ $el.append(new Option(val[0], val[1].toString(), false, true));
+ }
+ $el.trigger('change');
+ }
+ else {
+ $el.val(/** @type {string} */ (value)).trigger('change');
+ }
+ }
+ }
+ ),
+ );
computeEnables();
+ }
- // Name is not one of the presets, i.e user is creating a new preset
- if (!preset) {
- return;
- }
+ initializeSelect();
- for (const [name, value] of Object.entries(preset)) {
- const $el = $form.find(`.form--filter[data-name=${name}]`);
- if (value?.constructor == Array) {
- $el.val(null);
- for (const val of value) {
- $el.append(new Option(val[0], val[1].toString(), false, true));
- }
- $el.trigger('change');
- }
- else {
- $el.val(/** @type {string} */ (value)).trigger('change');
- }
+ // Enable saving when the filter is changed
+ $formFilters.on('change', computeEnables);
+ $isDefaultCheckbox.on('change', computeEnables);
+
+ async function saveFilter() {
+ if (!$form[0].reportValidity()) {
+ return;
}
- }));
- computeEnables();
- }
- initializeSelect();
+ const filter = /** @type {QPixelFilter} */ ({});
- // Enable saving when the filter is changed
- $formFilters.on('change', computeEnables);
- $isDefaultCheckbox.on('change', computeEnables);
+ for (const el of $formFilters) {
+ filter[el.dataset.name] = $(el).val();
+ }
- async function saveFilter() {
- if (!$form[0].reportValidity()) { return; }
+ await QPixel.setFilter($select.val()?.toString(), filter, categoryId, $isDefaultCheckbox.prop('checked'));
- const filter = /** @type {QPixelFilter} */({});
+ defaultFilter = categoryId ? await QPixel.defaultFilter(categoryId) : null;
- for (const el of $formFilters) {
- filter[el.dataset.name] = $(el).val();
+ // Reinitialize to get new options
+ await initializeSelect();
}
- await QPixel.setFilter($select.val()?.toString(), filter, categoryId, $isDefaultCheckbox.prop('checked'));
- defaultFilter = await QPixel.defaultFilter(categoryId);
+ $saveButton.on('click', saveFilter);
- // Reinitialize to get new options
- await initializeSelect();
- }
-
- $saveButton.on('click', saveFilter);
+ function clear() {
+ $select.val(null).trigger('change');
+ $form.find('.form--filter').val(null).trigger('change');
+ $isDefaultCheckbox.prop('checked', false);
+ computeEnables();
+ }
- function clear() {
- $select.val(null).trigger('change');
- $form.find('.form--filter').val(null).trigger('change');
- $isDefaultCheckbox.prop('checked', false);
- computeEnables();
- }
+ $deleteButton?.on('click', async (_evt) => {
+ if (confirm(`Are you sure you want to delete ${$select.val()}?`)) {
+ await QPixel.deleteFilter($select.val()?.toString());
+ // Reinitialize to get new options
+ await initializeSelect();
+ clear();
+ }
+ });
- $deleteButton?.on('click', async (_evt) => {
- if (confirm(`Are you sure you want to delete ${$select.val()}?`)) {
- await QPixel.deleteFilter($select.val()?.toString());
- // Reinitialize to get new options
- await initializeSelect();
- clear();
- }
+ $form.find('.filter-clear').on('click', clear);
});
-
- $form.find('.filter-clear').on('click', clear);
- });
});
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index fa7cc317d..adf289d1e 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -177,13 +177,12 @@ def delete_filter
end
def default_filter
- if user_signed_in? && params[:category]
+ if user_signed_in? && params[:category].present?
default_filter = helpers.default_filter(current_user.id, params[:category].to_i)
- render json: { status: 'success', success: true, name: default_filter&.name },
- status: 200
+ render json: { status: 'success', success: true, name: default_filter&.name }
else
render json: { status: 'failed', success: false },
- status: 400
+ status: :bad_request
end
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index b71561afd..f5b90f1b8 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -47,10 +47,9 @@ def preference_choice(pref_config)
end
end
- ##
- # Get the default filter for the specified user and category.
- # @param user_id [Integer]
- # @param category_id [Category]
+ # Get the default filter for a given user and category +id+.
+ # @param user_id [Integer] +id+ of the user to get default filter for
+ # @param category_id [Integer] +id+ of the category to get default filter for
# @return [Filter, nil]
def default_filter(user_id, category_id)
CategoryFilterDefault.find_by(user_id: user_id, category_id: category_id)&.filter
diff --git a/package-lock.json b/package-lock.json
index 8e19e8c1d..22257c281 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,7 +7,9 @@
"name": "qpixel",
"devDependencies": {
"@types/jquery": "^3.5.32",
+ "@types/prettier": "2.7.3",
"@types/select2": "^4.0.63",
+ "prettier-plugin-brace-style": "0.8.1",
"typescript": "5.6.3"
}
},
@@ -21,6 +23,13 @@
"@types/sizzle": "*"
}
},
+ "node_modules/@types/prettier": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz",
+ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/select2": {
"version": "4.0.63",
"resolved": "https://registry.npmjs.org/@types/select2/-/select2-4.0.63.tgz",
@@ -37,6 +46,49 @@
"integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==",
"dev": true
},
+ "node_modules/prettier": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-plugin-brace-style": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-brace-style/-/prettier-plugin-brace-style-0.8.1.tgz",
+ "integrity": "sha512-nhyuc8ETHk/TDNegbH3t6xq+WRVsAqIRLfT3sqZZO+PU8dSQ8dZVhw9FBazFF7FwR0masRx/TbrSdS66IC37Zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "zod": "3.22.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "prettier": "^3",
+ "prettier-plugin-astro": "*",
+ "prettier-plugin-svelte": "*"
+ },
+ "peerDependenciesMeta": {
+ "prettier-plugin-astro": {
+ "optional": true
+ },
+ "prettier-plugin-svelte": {
+ "optional": true
+ }
+ }
+ },
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
@@ -49,6 +101,16 @@
"engines": {
"node": ">=14.17"
}
+ },
+ "node_modules/zod": {
+ "version": "3.22.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
+ "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
},
"dependencies": {
@@ -61,6 +123,12 @@
"@types/sizzle": "*"
}
},
+ "@types/prettier": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz",
+ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==",
+ "dev": true
+ },
"@types/select2": {
"version": "4.0.63",
"resolved": "https://registry.npmjs.org/@types/select2/-/select2-4.0.63.tgz",
@@ -76,11 +144,33 @@
"integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==",
"dev": true
},
+ "prettier": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+ "dev": true,
+ "peer": true
+ },
+ "prettier-plugin-brace-style": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-brace-style/-/prettier-plugin-brace-style-0.8.1.tgz",
+ "integrity": "sha512-nhyuc8ETHk/TDNegbH3t6xq+WRVsAqIRLfT3sqZZO+PU8dSQ8dZVhw9FBazFF7FwR0masRx/TbrSdS66IC37Zw==",
+ "dev": true,
+ "requires": {
+ "zod": "3.22.4"
+ }
+ },
"typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true
+ },
+ "zod": {
+ "version": "3.22.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
+ "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
+ "dev": true
}
}
}
diff --git a/package.json b/package.json
index c3e3860bf..69730e973 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,9 @@
},
"devDependencies": {
"@types/jquery": "^3.5.32",
+ "@types/prettier": "2.7.3",
"@types/select2": "^4.0.63",
+ "prettier-plugin-brace-style": "0.8.1",
"typescript": "5.6.3"
}
}
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
index 055032bad..e257ae957 100644
--- a/test/controllers/users_controller_test.rb
+++ b/test/controllers/users_controller_test.rb
@@ -553,6 +553,18 @@ class UsersControllerTest < ActionController::TestCase
end
end
+ test 'default_filter should correctly respond to missing category' do
+ sign_in users(:standard_user)
+ try_default_filter(nil)
+ assert_response(:bad_request)
+ end
+
+ test 'default_filter should correctly get default category filters' do
+ sign_in users(:standard_user)
+ try_default_filter(categories(:main))
+ assert_json_success
+ end
+
private
def create_other_user
@@ -563,6 +575,13 @@ def create_other_user
other_user
end
+ def try_default_filter(category)
+ get :default_filter, params: {
+ category: category&.id,
+ format: :json
+ }
+ end
+
def try_save_filter(**opts)
filter = { name: 'test filter' }.merge(opts)
post :set_filter, params: filter.merge({ format: :json })