Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/mblajek/fddsz into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
mblajek committed Nov 14, 2023
2 parents 2b457ea + 7abdb7f commit 9c12f57
Show file tree
Hide file tree
Showing 17 changed files with 212 additions and 157 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Select, SelectItem} from "components/ui/form/Select";
import {useLangFunc} from "components/utils";
import {BoolColumnFilter, NullColumnFilter} from "data-access/memo-api/tquery/types";
import {createComputed, createMemo, createSignal} from "solid-js";
import {Select, SelectItem} from "../../form/Select";
import s from "./ColumnFilterController.module.scss";
import {makeSelectItem} from "./select_items";
import {FilterControl} from "./types";
Expand Down
5 changes: 4 additions & 1 deletion resources/js/components/utils/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {TOptions} from "i18next";
* A wrapper for useTransContext with the basic overload options, and with better
* types for usage as TSX attributes (no null returned).
*/
export function useLangFunc(): LangPrefixFunc {
export function useLangFunc(): LangFunc {
const transContext = useTransContext();
if (!transContext) {
throw new Error(`Called useLangFunc outside of the provider.`);
Expand All @@ -24,6 +24,9 @@ export function getLangEntryFunc(func: LangPrefixFunc, key: string): LangEntryFu
/** A function for getting the translation values from under a particular key prefix. */
export type LangPrefixFunc = (subKey: string, options?: TOptions) => string;

/** A function for getting the translation values at the top level. */
export type LangFunc = LangPrefixFunc;

function isLangPrefixParams(
params: Parameters<LangEntryFunc> | Parameters<LangPrefixFunc>,
): params is Parameters<LangPrefixFunc> {
Expand Down
31 changes: 31 additions & 0 deletions resources/js/components/utils/one_time_effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {createEffect} from "solid-js";
import {Accessor, createSignal} from "solid-js";

/**
* Creates an effect that runs once, when the input returns a non-undefined value for the first time
* (which may be immediately).
*
* Returns a function that can be called to reënable the effect for another single call of
* the effect function.
*/
export function createOneTimeEffect<T>({
input,
effect,
initialActive = true,
}: {
input: Accessor<T | undefined>;
effect: (value: T) => void;
initialActive?: boolean;
}) {
const [active, setActive] = createSignal(initialActive);
createEffect(() => {
if (active()) {
const inputVal = input();
if (inputVal !== undefined) {
effect(inputVal);
setActive(false);
}
}
});
return () => setActive(true);
}
6 changes: 3 additions & 3 deletions resources/js/data-access/memo-api/attributes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {createQuery} from "@tanstack/solid-query";
import {LangPrefixFunc, NON_NULLABLE, useLangFunc} from "components/utils";
import {LangFunc, NON_NULLABLE, useLangFunc} from "components/utils";
import {translationsLoaded} from "i18n_loader";
import {createMemo} from "solid-js";
import {FacilityIdOrGlobal, activeFacilityId} from "state/activeFacilityId.state";
Expand All @@ -17,7 +17,7 @@ export class Attributes {
readonly byModel: ReadonlyMap<string, Attribute[]>,
) {}

static fromResources(t: LangPrefixFunc, resources: AttributeResource[], dictionaries: Dictionaries) {
static fromResources(t: LangFunc, resources: AttributeResource[], dictionaries: Dictionaries) {
return Attributes.fromAttributes(resources.map((resource) => Attribute.fromResource(t, resource, dictionaries)));
}

Expand Down Expand Up @@ -71,7 +71,7 @@ export class Attribute {
readonly multiple: boolean | undefined,
) {}

static fromResource(t: LangPrefixFunc, resource: AttributeResource, dictionaries: Dictionaries) {
static fromResource(t: LangFunc, resource: AttributeResource, dictionaries: Dictionaries) {
return new Attribute(
resource,
resource.id,
Expand Down
8 changes: 4 additions & 4 deletions resources/js/data-access/memo-api/dictionaries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {createQuery} from "@tanstack/solid-query";
import {LangPrefixFunc, NON_NULLABLE, useLangFunc} from "components/utils";
import {LangFunc, NON_NULLABLE, useLangFunc} from "components/utils";
import {translationsLoaded} from "i18n_loader";
import {createMemo} from "solid-js";
import {FacilityIdOrGlobal, activeFacilityId} from "state/activeFacilityId.state";
Expand All @@ -19,7 +19,7 @@ export class Dictionaries {
readonly byName: ReadonlyMap<string, Dictionary>,
) {}

static fromResources(t: LangPrefixFunc, resources: DictionaryResource[]) {
static fromResources(t: LangFunc, resources: DictionaryResource[]) {
return Dictionaries.fromDictionaries(resources.map((resource) => Dictionary.fromResource(t, resource)));
}

Expand Down Expand Up @@ -71,7 +71,7 @@ export class Dictionary {
this.activePositions = this.allPositions.filter((position) => !position.resource.isDisabled);
}

static fromResource(t: LangPrefixFunc, resource: DictionaryResource) {
static fromResource(t: LangFunc, resource: DictionaryResource) {
const isTranslatable = isNameTranslatable(resource.name);
return new Dictionary(
resource,
Expand Down Expand Up @@ -106,7 +106,7 @@ export class Position {
readonly disabled;

constructor(
t: LangPrefixFunc,
t: LangFunc,
readonly resource: PositionResource,
/** The name of the dictionary, if it's a translatable name. */
readonly dictionaryTranslatableName: string | undefined,
Expand Down
9 changes: 6 additions & 3 deletions resources/js/data-access/memo-api/groups/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {parseGetResponse} from "../utils";
* @see {@link http://localhost:9081/api/documentation#/User local docs}
*/
export namespace User {
const getStatus = (facilityId?: string, config?: Api.Config) =>
const getStatus = (facilityId?: Api.Id, config?: Api.Config) =>
V1.get<Api.Response.Get<GetStatusData>>(facilityId ? `/user/status/${facilityId}` : "/user/status", config).then(
parseGetResponse,
);
Expand All @@ -23,6 +23,9 @@ export namespace User {
export const changePassword = (data: ChangePasswordRequest, config?: Api.Config) =>
V1.post<Api.Response.Post>("/user/password", data, config);

export const setLastLoginFacilityId = (lastLoginFacilityId: Api.Id, config?: Api.Config) =>
V1.patch("/user", {lastLoginFacilityId}, config);

export type GetStatusData = {
user: UserResource;
permissions: PermissionsResource;
Expand All @@ -43,7 +46,7 @@ export namespace User {
export const keys = {
all: () => ["user"] as const,
statusAll: () => [...keys.all(), "status"] as const,
status: (facilityId?: string) => [...keys.statusAll(), facilityId] as const,
status: (facilityId?: Api.Id) => [...keys.statusAll(), facilityId] as const,
};

type PermissionsFacilityKeys = "facilityId" | "facilityMember" | "facilityClient" | "facilityStaff" | "facilityAdmin";
Expand Down Expand Up @@ -77,7 +80,7 @@ export namespace User {
}) satisfies SolidQueryOptions<GetStatusWithoutFacilityData>;

/** Query options for user status with facility permissions. */
export const statusWithFacilityPermissionsQueryOptions = (facilityId: string) =>
export const statusWithFacilityPermissionsQueryOptions = (facilityId: Api.Id) =>
({
queryFn: ({signal}) => getStatus(facilityId, {signal}),
queryKey: keys.status(facilityId),
Expand Down
17 changes: 2 additions & 15 deletions resources/js/features/authentication/pages/Login.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,8 @@ import {LoginForm} from "../forms/login/Login.form";
*/
const LoginPage: VoidComponent = () => {
const statusQuery = createQuery(User.statusQueryOptions);

createEffect(() => {
if (statusQuery.isSuccess) {
if (statusQuery.data.user.lastLoginFacilityId) setActiveFacilityId(statusQuery.data.user.lastLoginFacilityId);
}
});

createEffect(() => {
LoginForm.showModal(statusQuery.isError);
});

onMount(() => {
setActiveFacilityId(undefined);
});

onMount(() => setActiveFacilityId(undefined));
createEffect(() => LoginForm.showModal(statusQuery.isError));
return (
<Page title="Logowanie">
<LoginForm.LoginModal />
Expand Down
81 changes: 41 additions & 40 deletions resources/js/features/root/components/header/FacilityControl.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,62 @@
import {useNavigate, useParams} from "@solidjs/router";
import {createQuery} from "@tanstack/solid-query";
import {Select} from "components/ui/form/Select";
import {createOneTimeEffect} from "components/utils/one_time_effect";
import {System, User} from "data-access/memo-api/groups";
import {For, Match, Show, Switch, VoidComponent, createEffect, createMemo} from "solid-js";
import {Match, Show, Switch, VoidComponent, createMemo} from "solid-js";
import {activeFacilityId, setActiveFacilityId} from "state/activeFacilityId.state";

export const FacilityControl: VoidComponent = () => {
const navigate = useNavigate();
const params = useParams();

const facilitiesQuery = createQuery(System.facilitiesQueryOptions);
const statusQuery = createQuery(User.statusQueryOptions);

const facilities = createMemo(
const invalidate = User.useInvalidator();
const userFacilities = createMemo(
() =>
facilitiesQuery.data?.filter(
(facility) => statusQuery.data?.members.find((member) => member.facilityId === facility.id),
),
facilitiesQuery.data
?.filter((facility) => statusQuery.data?.members.find((member) => member.facilityId === facility.id))
.sort((a, b) => a.name.localeCompare(b.name)),
);

// TODO: it just works, may be wrong. Maybe there is a better way of handling 'global mutable state' :D
createEffect(() => {
if (params.facilityUrl) {
const facility = facilities()?.find((facility) => facility.url === params.facilityUrl);

if (!facility) return;

if (statusQuery.data?.members.find((member) => member.facilityId === facility.id))
setActiveFacilityId(facility.id);
}
createOneTimeEffect({
input: () => {
const facilities = userFacilities();
if (!facilities || !statusQuery.isSuccess) {
return undefined;
}
// Use the facility from the URL, if not present (e.g. not on a facility-specific page) use the
// last login facility, and finally use any (the first) facility, so that some facility is always
// selected.
return (
facilities.find(({url}) => url === params.facilityUrl) ||
facilities.find(({id}) => id === statusQuery.data!.user.lastLoginFacilityId) ||
facilities[0]
)?.id;
},
effect: setActiveFacilityId,
});

// TODO: it just works, may be wrong. Maybe there is a better way of handling 'global mutable state' :D
createEffect(() => {
const facilitiesSub = facilities();
if (facilitiesSub?.length === 1) setActiveFacilityId(facilitiesSub[0]?.id);
});

return (
<Show when={facilities()}>
{(facilities) => (
<Show when={userFacilities()}>
{(userFacilities) => (
<Switch>
<Match when={facilities().length === 1}>
<p>{facilities().at(0)?.name}</p>
<Match when={userFacilities().length === 1}>
<p>{userFacilities()[0]!.name}</p>
</Match>
<Match when={facilities().length > 1}>
<select
class="border border-gray-200 rounded"
<Match when={userFacilities().length > 1}>
<Select
name="activeFacilityId"
items={userFacilities().map(({id, name}) => ({value: id, label: () => name}))}
nullable={false}
value={activeFacilityId()}
onChange={(e) => {
const url = facilities().find((facility) => facility.id === e.target.value)?.url;

setActiveFacilityId(e.target.value);
if (url) navigate(`/${url}`);
onValueChange={(facilityId) => {
if (facilityId !== activeFacilityId()) {
setActiveFacilityId(facilityId);
const url = userFacilities().find((facility) => facility.id === facilityId)?.url;
if (url) navigate(`/${url}`);
}
User.setLastLoginFacilityId(facilityId!).then(() => invalidate.statusAndFacilityPermissions());
}}
>
<For each={facilities()}>{(facility) => <option value={facility.id}>{facility.name}</option>}</For>
</select>
/>
</Match>
</Switch>
)}
Expand Down
2 changes: 1 addition & 1 deletion resources/js/features/root/components/header/UserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {createMutation, createQuery} from "@tanstack/solid-query";
import {Button} from "components/ui/Button";
import {DATE_TIME_WITH_WEEKDAY_FORMAT, currentTime, useLangFunc} from "components/utils";
import {User} from "data-access/memo-api/groups";
import {HeaderSeparator} from "features/root/layout";
import {HeaderSeparator} from "features/root/layout/HeaderSeparator";
import {PasswordChangeForm} from "features/user-panel/PasswordChange.form";
import {HiOutlineCheckCircle, HiOutlinePower, HiOutlineXCircle} from "solid-icons/hi";
import {TbPassword} from "solid-icons/tb";
Expand Down
2 changes: 0 additions & 2 deletions resources/js/features/root/components/header/index.ts

This file was deleted.

2 changes: 0 additions & 2 deletions resources/js/features/root/components/navbar/index.ts

This file was deleted.

7 changes: 3 additions & 4 deletions resources/js/features/root/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import {VoidComponent} from "solid-js";
import {FacilityControl, UserInfo} from "../components/header";
import {HeaderSeparator} from "./HeaderSeparator";
import {FacilityControl} from "../components/header/FacilityControl";
import {UserInfo} from "../components/header/UserInfo";
import s from "./layout.module.scss";

export const Header: VoidComponent = () => {
return (
<header class={s.header}>
<div class="flex-grow" />
<FacilityControl />
<HeaderSeparator />
<div class="flex-grow" />
<UserInfo />
</header>
);
Expand Down
Loading

0 comments on commit 9c12f57

Please sign in to comment.