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 })