diff --git a/resources/js/components/ui/SearchInput.tsx b/resources/js/components/ui/SearchInput.tsx new file mode 100644 index 000000000..01f6d03a8 --- /dev/null +++ b/resources/js/components/ui/SearchInput.tsx @@ -0,0 +1,54 @@ +import {FiDelete} from "solid-icons/fi"; +import {Show, VoidComponent, createEffect, createSignal, on} from "solid-js"; +import {cx, htmlAttributes} from "../utils"; +import {Button} from "./Button"; +import {TextInput} from "./TextInput"; +import {splitProps} from "solid-js"; + +interface Props extends htmlAttributes.input { + readonly divClass?: string; +} + +export const SearchInput: VoidComponent = (allProps) => { + const [props, inputProps] = splitProps(allProps, ["divClass"]); + let ref: HTMLInputElement | undefined; + const [value, setValue] = createSignal(""); + function setNow() { + setValue(ref?.value || ""); + } + createEffect( + on( + () => inputProps.value, + () => setTimeout(setNow, 0), + ), + ); + return ( +
+ { + ref = input; + (inputProps.ref as (elem: HTMLInputElement) => void)?.(input); + }} + /> + + + +
+ ); +}; diff --git a/resources/js/components/ui/Table/FilterIcon.tsx b/resources/js/components/ui/Table/FilterIconButton.tsx similarity index 52% rename from resources/js/components/ui/Table/FilterIcon.tsx rename to resources/js/components/ui/Table/FilterIconButton.tsx index 1205b581c..0b8086a97 100644 --- a/resources/js/components/ui/Table/FilterIcon.tsx +++ b/resources/js/components/ui/Table/FilterIconButton.tsx @@ -1,36 +1,36 @@ -import {useLangFunc} from "components/utils"; +import {cx, useLangFunc} from "components/utils"; import {TbFilter, TbFilterOff} from "solid-icons/tb"; import {VoidComponent, createSignal} from "solid-js"; import {Dynamic} from "solid-js/web"; import {Button} from "../Button"; interface Props { - readonly isFiltering?: boolean; readonly class?: string; - readonly onClear?: () => void; + readonly isFiltering: boolean; + readonly onClear: () => void; + readonly title?: string; } -export const FilterIcon: VoidComponent = (props) => { +export const FilterIconButton: VoidComponent = (props) => { const t = useLangFunc(); const [hover, setHover] = createSignal(false); return ( ); diff --git a/resources/js/components/ui/Table/TQueryTable.tsx b/resources/js/components/ui/Table/TQueryTable.tsx index f866f4f5c..95311e841 100644 --- a/resources/js/components/ui/Table/TQueryTable.tsx +++ b/resources/js/components/ui/Table/TQueryTable.tsx @@ -40,6 +40,7 @@ import { getBaseTableOptions, useTableCells, } from "."; +import {TableFiltersClearButton} from "./TableFiltersClearButton"; import {ColumnFilterController, FilteringParams} from "./tquery_filters/ColumnFilterController"; declare module "@tanstack/table-core" { @@ -213,7 +214,15 @@ export const TQueryTable: VoidComponent = (props) => { ), ); } - const {columnVisibility, globalFilter, getColumnFilter, sorting, pagination} = requestController; + const { + columnVisibility, + globalFilter, + getColumnFilter, + columnsWithActiveFilters, + clearColumnFilters, + sorting, + pagination, + } = requestController; if (props.staticPersistenceKey) { createLocalStoragePersistence({ key: `TQueryTable:${props.staticPersistenceKey}`, @@ -325,13 +334,17 @@ export const TQueryTable: VoidComponent = (props) => { mode={props.mode} rowsIteration="Index" aboveTable={() => ( -
- +
+ +
)} belowTable={() => ( -
+
{props.customSectionBelowTable} diff --git a/resources/js/components/ui/Table/TableFiltersClearButton.tsx b/resources/js/components/ui/Table/TableFiltersClearButton.tsx new file mode 100644 index 000000000..109efbbdd --- /dev/null +++ b/resources/js/components/ui/Table/TableFiltersClearButton.tsx @@ -0,0 +1,28 @@ +import {useLangFunc} from "components/utils"; +import {VoidComponent} from "solid-js"; +import {FilterIconButton} from "./FilterIconButton"; +import {useTable} from "./TableContext"; + +interface Props { + readonly columnsWithActiveFilters: readonly string[]; + readonly clearColumnFilters: () => void; +} + +export const TableFiltersClearButton: VoidComponent = (props) => { + const t = useLangFunc(); + const table = useTable(); + return ( + 0} + onClear={props.clearColumnFilters} + title={ + props.columnsWithActiveFilters.length + ? `${t("tables.filter.column_filters_set")}\n${props.columnsWithActiveFilters + .map((column) => `- ${table.options.meta?.translations?.columnNames(column)}`) + .join("\n")}\n${t("tables.filter.click_to_clear")}` + : t("tables.filter.column_filters_cleared") + } + /> + ); +}; diff --git a/resources/js/components/ui/Table/TableSearch.tsx b/resources/js/components/ui/Table/TableSearch.tsx index cfa2e590c..923dbd767 100644 --- a/resources/js/components/ui/Table/TableSearch.tsx +++ b/resources/js/components/ui/Table/TableSearch.tsx @@ -1,27 +1,25 @@ -import {htmlAttributes, useLangFunc} from "components/utils"; -import {ParentProps, VoidComponent, createComputed, createSignal, splitProps} from "solid-js"; +import {cx, useLangFunc} from "components/utils"; +import {ParentProps, VoidComponent} from "solid-js"; import {useTable} from "."; +import {SearchInput} from "../SearchInput"; -interface Props extends htmlAttributes.div { +interface Props { + readonly divClass?: string; readonly placeholder?: string; } -export const TableSearch: VoidComponent> = (allProps) => { - const [props, divProps] = splitProps(allProps, ["placeholder"]); +export const TableSearch: VoidComponent> = (props) => { const t = useLangFunc(); const table = useTable(); - const [query, setQuery] = createSignal(table.getState().globalFilter); - createComputed(() => table.setGlobalFilter(query())); + const globalFilter = (): string => table.getState().globalFilter; return ( -
- setQuery(value)} - /> -
+ table.setGlobalFilter(value)} + /> ); }; diff --git a/resources/js/components/ui/Table/index.ts b/resources/js/components/ui/Table/index.ts index 7195a82f3..1dd866376 100644 --- a/resources/js/components/ui/Table/index.ts +++ b/resources/js/components/ui/Table/index.ts @@ -1,5 +1,5 @@ export * from "./ColumnName"; -export * from "./FilterIcon"; +export * from "./FilterIconButton"; export * from "./Header"; export * from "./IdColumn"; export * from "./Pagination"; diff --git a/resources/js/components/ui/Table/tquery_filters/ColumnFilterController.module.scss b/resources/js/components/ui/Table/tquery_filters/ColumnFilterController.module.scss index 76863a041..cb3abbad0 100644 --- a/resources/js/components/ui/Table/tquery_filters/ColumnFilterController.module.scss +++ b/resources/js/components/ui/Table/tquery_filters/ColumnFilterController.module.scss @@ -41,8 +41,4 @@ } } } - - .filterIcon { - @apply mb-1; - } } diff --git a/resources/js/components/ui/Table/tquery_filters/ColumnFilterController.tsx b/resources/js/components/ui/Table/tquery_filters/ColumnFilterController.tsx index 3e22af686..18bdfa28c 100644 --- a/resources/js/components/ui/Table/tquery_filters/ColumnFilterController.tsx +++ b/resources/js/components/ui/Table/tquery_filters/ColumnFilterController.tsx @@ -1,5 +1,5 @@ import {JSX, Show, VoidComponent, mergeProps} from "solid-js"; -import {FilterIcon, useTable} from ".."; +import {FilterIconButton, useTable} from ".."; import {BoolFilterControl} from "./BoolFilterControl"; import s from "./ColumnFilterController.module.scss"; import {DateTimeFilterControl} from "./DateTimeFilterControl"; @@ -71,7 +71,9 @@ export const ColumnFilterController: VoidComponent = (props) {(filterControl) => ( <>
{filterControl()()}
- props.setFilter(undefined)} /> +
+ props.setFilter(undefined)} /> +
)} diff --git a/resources/js/components/ui/Table/tquery_filters/TextualFilterControl.tsx b/resources/js/components/ui/Table/tquery_filters/TextualFilterControl.tsx index 12cfcf936..af6311dc3 100644 --- a/resources/js/components/ui/Table/tquery_filters/TextualFilterControl.tsx +++ b/resources/js/components/ui/Table/tquery_filters/TextualFilterControl.tsx @@ -1,3 +1,4 @@ +import {SearchInput} from "components/ui/SearchInput"; import {Select, SelectItem} from "components/ui/form/Select"; import {debouncedFilterTextAccessor, useLangFunc} from "components/utils"; import {FilterH} from "data-access/memo-api/tquery/filter_utils"; @@ -12,9 +13,9 @@ interface StringColumnProps extends FilterControlProps { } export const TextualFilterControl: VoidComponent = (props) => { + const t = useLangFunc(); const [mode, setMode] = createSignal("~"); const [text, setText] = createSignal(""); - const t = useLangFunc(); createComputed(() => { if (!props.filter) { setMode("~"); @@ -103,11 +104,10 @@ export const TextualFilterControl: VoidComponent = (props) => />
- = (inputProps) => ( + +); diff --git a/resources/js/components/ui/form/TextField.tsx b/resources/js/components/ui/form/TextField.tsx index 32f41b9bf..beb1b3eee 100644 --- a/resources/js/components/ui/form/TextField.tsx +++ b/resources/js/components/ui/form/TextField.tsx @@ -2,6 +2,7 @@ import {htmlAttributes} from "components/utils"; import {JSX, VoidComponent, splitProps} from "solid-js"; import {FieldBox} from "./FieldBox"; import {labelIdForField} from "./FieldLabel"; +import {TextInput} from "../TextInput"; export interface TextFieldProps extends Pick< @@ -17,14 +18,11 @@ export const TextField: VoidComponent = (allProps) => { const [props, inputProps] = splitProps(allProps, ["name", "label"]); return ( - diff --git a/resources/js/data-access/memo-api/resources/adminUser.resource.ts b/resources/js/data-access/memo-api/resources/adminUser.resource.ts index 975322b56..56e00cedd 100644 --- a/resources/js/data-access/memo-api/resources/adminUser.resource.ts +++ b/resources/js/data-access/memo-api/resources/adminUser.resource.ts @@ -3,51 +3,51 @@ import {MemberResource} from "./member.resource"; /** * @see `/app/Http/Resources/Admin/AdminUserResource.php` */ -export type AdminUserResource = { +export interface AdminUserResource { /** * admin user identifier * @type {string(uuid)} * @example '67da972b-34d7-4f89-b8ae-322d96b4954d' */ - id: string; + readonly id: string; /** * admin user full name * @type {string} */ - name: string; + readonly name: string; /** * email address * @type {string} * @example 'test@test.pl' */ - email: string | null; + readonly email: string | null; /** * facility identifier where the user was last logged in * @type {string(uuid)} * @example '67da972b-34d7-4f89-b8ae-322d96b4954d' */ - lastLoginFacilityId: string | null; - passwordExpireAt: string | null; - hasPassword: boolean; - createdAt: string; - updatedAt: string; - hasEmailVerified: boolean; + readonly lastLoginFacilityId: string | null; + readonly passwordExpireAt: string | null; + readonly hasPassword: boolean; + readonly createdAt: string; + readonly updatedAt: string; + readonly hasEmailVerified: boolean; /** * identifier of a user who created this user * @type {string(uuid)} * @example '67da972b-34d7-4f89-b8ae-322d96b4954d' */ - createdBy: string; - hasGlobalAdmin: boolean; + readonly createdBy: string; + readonly hasGlobalAdmin: boolean; /** * array of members * @type {MemberResource[]} */ - members: MemberResource[]; -}; + readonly members: readonly MemberResource[]; +} /** The user resource used for creation. */ export type AdminUserResourceForCreate = Pick< AdminUserResource, "name" | "email" | "hasEmailVerified" | "passwordExpireAt" | "hasGlobalAdmin" -> & {password: string | null}; +> & {readonly password: string | null}; diff --git a/resources/js/data-access/memo-api/resources/dictionary.resource.ts b/resources/js/data-access/memo-api/resources/dictionary.resource.ts index 08a8aca71..9059e1a9f 100644 --- a/resources/js/data-access/memo-api/resources/dictionary.resource.ts +++ b/resources/js/data-access/memo-api/resources/dictionary.resource.ts @@ -18,7 +18,7 @@ export interface DictionaryResource { readonly facilityId: string | null; readonly name: DictionaryName; /** The positions in the dictionary, sorted by their sort order. */ - readonly positions: PositionResource[]; + readonly positions: readonly PositionResource[]; /** * Whether the dictionary is unmodifiable even by the admin. * Fixed dictionaries can be referenced by name in the code. diff --git a/resources/js/data-access/memo-api/resources/facility.resource.ts b/resources/js/data-access/memo-api/resources/facility.resource.ts index 78f0ce5f1..867ccb4a8 100644 --- a/resources/js/data-access/memo-api/resources/facility.resource.ts +++ b/resources/js/data-access/memo-api/resources/facility.resource.ts @@ -1,8 +1,8 @@ /** * @see `/app/Http/Resources/FacilityResource.php` */ -export type FacilityResource = { - id: string; - name: string; - url: string; -}; +export interface FacilityResource { + readonly id: string; + readonly name: string; + readonly url: string; +} diff --git a/resources/js/data-access/memo-api/resources/meeting.resource.ts b/resources/js/data-access/memo-api/resources/meeting.resource.ts index 48d3faabe..3153f1abd 100644 --- a/resources/js/data-access/memo-api/resources/meeting.resource.ts +++ b/resources/js/data-access/memo-api/resources/meeting.resource.ts @@ -4,28 +4,28 @@ * @see `/app/Http/Resources/MeetingResource.php` */ export interface MeetingResource { - id: string; - facilityId: string; - typeDictId: string; - notes: string; - date: string; - startDayminute: number; - durationMinutes: number; - statusDictId: string; - createdBy: string; - isRemote: boolean; - staff: MeetingAttendantResource[]; - clients: MeetingAttendantResource[]; - resources: MeetingResourceResource[]; + readonly id: string; + readonly facilityId: string; + readonly typeDictId: string; + readonly notes: string; + readonly date: string; + readonly startDayminute: number; + readonly durationMinutes: number; + readonly statusDictId: string; + readonly createdBy: string; + readonly isRemote: boolean; + readonly staff: readonly MeetingAttendantResource[]; + readonly clients: readonly MeetingAttendantResource[]; + readonly resources: readonly MeetingResourceResource[]; } export interface MeetingAttendantResource { - userId: string; - attendanceStatusDictId: string; + readonly userId: string; + readonly attendanceStatusDictId: string; } export interface MeetingResourceResource { - resourceDictId: string; + readonly resourceDictId: string; } export type MeetingResourceForCreate = Pick< diff --git a/resources/js/data-access/memo-api/resources/member.resource.ts b/resources/js/data-access/memo-api/resources/member.resource.ts index 36cb57429..24da7f4ed 100644 --- a/resources/js/data-access/memo-api/resources/member.resource.ts +++ b/resources/js/data-access/memo-api/resources/member.resource.ts @@ -1,11 +1,11 @@ /** * @see `/app/Http/Resources/MemeberResource.php` */ -export type MemberResource = { - id: string; - userId: string; - facilityId: string; - hasFacilityAdmin: boolean; - staffMemberId: string | null; - clientId: string | null; -}; +export interface MemberResource { + readonly id: string; + readonly userId: string; + readonly facilityId: string; + readonly hasFacilityAdmin: boolean; + readonly staffMemberId: string | null; + readonly clientId: string | null; +} diff --git a/resources/js/data-access/memo-api/resources/permissions.resource.ts b/resources/js/data-access/memo-api/resources/permissions.resource.ts index 243443d67..a42ec6a60 100644 --- a/resources/js/data-access/memo-api/resources/permissions.resource.ts +++ b/resources/js/data-access/memo-api/resources/permissions.resource.ts @@ -2,13 +2,13 @@ * @see `/app/Http/Resources/PermissionResource.php` */ export type PermissionsResource = { - userId: string; - facilityId: string; - unverified: boolean; - verified: boolean; - globalAdmin: boolean; - facilityMember: boolean; - facilityClient: boolean; - facilityStaff: boolean; - facilityAdmin: boolean; + readonly userId: string; + readonly facilityId: string; + readonly unverified: boolean; + readonly verified: boolean; + readonly globalAdmin: boolean; + readonly facilityMember: boolean; + readonly facilityClient: boolean; + readonly facilityStaff: boolean; + readonly facilityAdmin: boolean; }; diff --git a/resources/js/data-access/memo-api/resources/user.resource.ts b/resources/js/data-access/memo-api/resources/user.resource.ts index 6ed0ba272..3fddf3b92 100644 --- a/resources/js/data-access/memo-api/resources/user.resource.ts +++ b/resources/js/data-access/memo-api/resources/user.resource.ts @@ -1,34 +1,34 @@ /** * @see `/app/Http/Resources/UserResource.php` */ -export type UserResource = { +export interface UserResource { /** * identifier * @type {string(uuid)} * @example '67da972b-34d7-4f89-b8ae-322d96b4954d' */ - id: string; + readonly id: string; /** * full name * @type {string} */ - name: string; + readonly name: string; /** * email address * @type {string(email)} * @example 'test@test.pl' */ - email: string; + readonly email: string; /** * facility identifier where the user was last logged in * @type {string(uuid)} * @example '67da972b-34d7-4f89-b8ae-322d96b4954d' */ - lastLoginFacilityId: string | null; + readonly lastLoginFacilityId: string | null; /** * password expiration date * @type {string(date-time)} * @example '2023-05-10T20:46:43Z' */ - passwordExpireAt: string; -}; + readonly passwordExpireAt: string; +} diff --git a/resources/js/data-access/memo-api/tquery/table.ts b/resources/js/data-access/memo-api/tquery/table.ts index b9f191593..1f31f23ea 100644 --- a/resources/js/data-access/memo-api/tquery/table.ts +++ b/resources/js/data-access/memo-api/tquery/table.ts @@ -4,7 +4,7 @@ import {AxiosError} from "axios"; import {TableTranslations} from "components/ui/Table"; import {FuzzyGlobalFilterConfig, buildFuzzyGlobalFilter} from "components/ui/Table/tquery_filters/fuzzy_filter"; import {NON_NULLABLE, debouncedFilterTextAccessor, useLangFunc} from "components/utils"; -import {Accessor, Signal, createComputed, createMemo, createSignal, on} from "solid-js"; +import {Accessor, Signal, batch, createComputed, createMemo, createSignal, on} from "solid-js"; import {translateError} from "../error_util"; import {Api} from "../types"; import {FilterH, FilterReductor} from "./filter_utils"; @@ -25,6 +25,8 @@ interface RequestController { readonly columnVisibility: Signal; readonly globalFilter: Signal; readonly getColumnFilter: (column: ColumnName) => Signal; + readonly columnsWithActiveFilters: Accessor; + readonly clearColumnFilters: () => void; readonly sorting: Signal; readonly pagination: Signal; } @@ -73,6 +75,17 @@ export function createTableRequestCreator({ } return signal; } + const columnsWithActiveFilters = () => + Object.entries(columnFilters()) + .map(([column, filter]) => filter[0]() && column) + .filter(NON_NULLABLE); + function clearColumnFilters() { + batch(() => { + for (const signal of Object.values(columnFilters())) { + signal[1](undefined); + } + }); + } const defaultColumnVisibility = () => getDefaultColumnVisibility(columnsConfig()); // Initialise the request parts based on the config. createComputed(() => { @@ -189,6 +202,8 @@ export function createTableRequestCreator({ columnVisibility: [columnVisibility, setColumnVisibility], globalFilter: [globalFilter, setGlobalFilter], getColumnFilter, + columnsWithActiveFilters, + clearColumnFilters, sorting: [sorting, setSorting], pagination: [pagination, setPagination], }, diff --git a/resources/lang/pl_PL/forms.yml b/resources/lang/pl_PL/forms.yml index f90bba5b3..5255229aa 100644 --- a/resources/lang/pl_PL/forms.yml +++ b/resources/lang/pl_PL/forms.yml @@ -26,7 +26,7 @@ user_edit: # Alternative to password, if the password is changed: newPassword: "Nowe hasło" password_empty_to_leave_unchanged: "Puste — brak zmiany hasła" - global_admin_requires_password: "Globalny administrator musi mieć ustawione hasło" + global_admin_requires_password: "Globalny administrator musi posiadać hasło (być użytkownikiem Memo)" has_password_requires_email: "Użytkownik Memo musi posiadać adres e-mail" validation: cannot_remove_own_global_admin: "Użytkownik nie może odebrać sobie uprawnień globalnego administratora." diff --git a/resources/lang/pl_PL/tables.yml b/resources/lang/pl_PL/tables.yml index 29cb77cc0..25db02232 100644 --- a/resources/lang/pl_PL/tables.yml +++ b/resources/lang/pl_PL/tables.yml @@ -81,6 +81,8 @@ filter: filter_for: "filtr kolumny {{data}}" filter_cleared: "Brak filtra" filter_set: "Filtr aktywny" + column_filters_cleared: "Brak filtrów w kolumnach" + column_filters_set: "Filtry w kolumnach aktywne:" click_to_clear: "Kliknij aby wyczyścić" click_to_sync_date_range: "Kliknij aby ustawić jednodniowy zakres" click_to_sync_number_range: "Kliknij aby ustawić max = min"