mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 23:58:36 -05:00
Scrims association filter
This commit is contained in:
@@ -181,3 +181,44 @@ describe("isVisible", () => {
|
||||
expect(Association.isVisible(args)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mentionsAssociation", () => {
|
||||
test.each([
|
||||
{ why: "no visibility", visibility: null, expected: false },
|
||||
{
|
||||
why: "current association",
|
||||
visibility: { forAssociation: 1 },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
why: "other association",
|
||||
visibility: { forAssociation: 2 },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
why: "virtual association",
|
||||
visibility: { forAssociation: "+1" as const },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
why: "association later in the schedule",
|
||||
visibility: {
|
||||
forAssociation: 2,
|
||||
notFoundInstructions: [{ at: 0, forAssociation: 1 }],
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
why: "schedule going public only",
|
||||
visibility: {
|
||||
forAssociation: 2,
|
||||
notFoundInstructions: [{ at: 0, forAssociation: null }],
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
])("$why", ({ visibility, expected }) => {
|
||||
expect(
|
||||
Association.mentionsAssociation({ visibility, associationId: 1 }),
|
||||
).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,6 +53,24 @@ export function isVisible(args: IsVisibleArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether the association is in the visibility at any point of its schedule, not only right now. */
|
||||
export function mentionsAssociation({
|
||||
visibility,
|
||||
associationId,
|
||||
}: {
|
||||
visibility: AssociationVisibility | null;
|
||||
associationId: number;
|
||||
}) {
|
||||
if (!visibility) return false;
|
||||
|
||||
return (
|
||||
visibility.forAssociation === associationId ||
|
||||
(visibility.notFoundInstructions ?? []).some(
|
||||
(instruction) => instruction.forAssociation === associationId,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isPublic(args: Omit<IsVisibleArgs, "associations">) {
|
||||
return isVisible({
|
||||
associations: null,
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type MetaFunction, Outlet, useLoaderData } from "react-router";
|
||||
import { ActionButton } from "~/components/ActionButton";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { LinkButton, SendouButton } from "~/components/elements/Button";
|
||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
loader,
|
||||
} from "~/features/associations/loaders/associations.server";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { scrimsByAssociationPage } from "~/features/scrims/scrims-urls";
|
||||
import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
|
||||
import { useHasPermission } from "~/modules/permissions/hooks";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
@@ -133,35 +134,43 @@ function AssociationSection({
|
||||
?.username,
|
||||
})}
|
||||
</div>
|
||||
{!canManage || newAdmin ? (
|
||||
<FormWithConfirm
|
||||
dialogHeading={
|
||||
newAdmin
|
||||
? t("scrims:associations.leave.titleWithNewAdmin", {
|
||||
name: association.name,
|
||||
username: newAdmin.username,
|
||||
})
|
||||
: t("scrims:associations.leave.title", {
|
||||
name: association.name,
|
||||
})
|
||||
}
|
||||
fields={[
|
||||
["_action", "LEAVE_ASSOCIATION"],
|
||||
["associationId", association.id],
|
||||
]}
|
||||
submitButtonText={t("scrims:associations.leave.action")}
|
||||
<div className="stack horizontal sm items-center my-2">
|
||||
<LinkButton
|
||||
to={scrimsByAssociationPage(association.id)}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
<SendouButton
|
||||
variant="minimal-destructive"
|
||||
type="submit"
|
||||
size="small"
|
||||
className="my-2"
|
||||
data-testid="leave-team-button"
|
||||
{t("scrims:associations.viewScrims")}
|
||||
</LinkButton>
|
||||
{!canManage || newAdmin ? (
|
||||
<FormWithConfirm
|
||||
dialogHeading={
|
||||
newAdmin
|
||||
? t("scrims:associations.leave.titleWithNewAdmin", {
|
||||
name: association.name,
|
||||
username: newAdmin.username,
|
||||
})
|
||||
: t("scrims:associations.leave.title", {
|
||||
name: association.name,
|
||||
})
|
||||
}
|
||||
fields={[
|
||||
["_action", "LEAVE_ASSOCIATION"],
|
||||
["associationId", association.id],
|
||||
]}
|
||||
submitButtonText={t("scrims:associations.leave.action")}
|
||||
>
|
||||
{t("scrims:associations.leave.action")}
|
||||
</SendouButton>
|
||||
</FormWithConfirm>
|
||||
) : null}
|
||||
<SendouButton
|
||||
variant="minimal-destructive"
|
||||
type="submit"
|
||||
size="small"
|
||||
data-testid="leave-team-button"
|
||||
>
|
||||
{t("scrims:associations.leave.action")}
|
||||
</SendouButton>
|
||||
</FormWithConfirm>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="stack sm mt-4">
|
||||
{association.members?.map((member) => (
|
||||
<AssociationMember
|
||||
|
||||
102
app/features/scrims/loaders/scrims.server.test.ts
Normal file
102
app/features/scrims/loaders/scrims.server.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
import * as AssociationFactory from "~/db/seed/factories/AssociationFactory";
|
||||
import * as ScrimPostFactory from "~/db/seed/factories/ScrimPostFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import type { SerializeFrom } from "~/utils/remix";
|
||||
import { wrappedLoader } from "~/utils/Test";
|
||||
import { scrimsPage } from "~/utils/urls";
|
||||
import { scrimsSearchParams } from "../scrims-search-params";
|
||||
import { loader } from "./scrims.server";
|
||||
|
||||
const users = UserFactory.pool();
|
||||
|
||||
const viewerId = () => users.id(1);
|
||||
const otherUserId = () => users.id(2);
|
||||
const requesterId = () => users.id(3);
|
||||
|
||||
const scrimsLoader = wrappedLoader<SerializeFrom<typeof loader>>({ loader });
|
||||
|
||||
describe("scrims loader", () => {
|
||||
beforeEach(async () => {
|
||||
await users.create(3);
|
||||
});
|
||||
|
||||
const createAssociation = () =>
|
||||
AssociationFactory.create(
|
||||
{ userId: otherUserId() },
|
||||
{ memberUserIds: [viewerId()] },
|
||||
);
|
||||
|
||||
const loadFilteredByAssociation = (associationId: number) =>
|
||||
scrimsLoader({
|
||||
user: viewerId(),
|
||||
url: scrimsSearchParams.href(scrimsPage(), { associationId }),
|
||||
});
|
||||
|
||||
test("only lists posts of the association filtered by", async () => {
|
||||
const association = await createAssociation();
|
||||
|
||||
const { id: associationPostId } = await ScrimPostFactory.create({
|
||||
users: [{ userId: otherUserId(), isOwner: 1 }],
|
||||
visibility: { forAssociation: association.id },
|
||||
});
|
||||
await ScrimPostFactory.create({
|
||||
users: [{ userId: otherUserId(), isOwner: 1 }],
|
||||
});
|
||||
|
||||
const data = await loadFilteredByAssociation(association.id);
|
||||
|
||||
expect(data.posts.neutral.map((post) => post.id)).toEqual([
|
||||
associationPostId,
|
||||
]);
|
||||
});
|
||||
|
||||
test("keeps the viewer's own public post listed while filtering by association", async () => {
|
||||
const association = await createAssociation();
|
||||
|
||||
const { id: ownPostId } = await ScrimPostFactory.create({
|
||||
users: [{ userId: viewerId(), isOwner: 1 }],
|
||||
});
|
||||
|
||||
const data = await loadFilteredByAssociation(association.id);
|
||||
|
||||
expect(data.posts.owned.map((post) => post.id)).toEqual([ownPostId]);
|
||||
});
|
||||
|
||||
test("keeps a booked scrim listed while filtering by association", async () => {
|
||||
const association = await createAssociation();
|
||||
|
||||
const { id: bookedPostId } = await ScrimPostFactory.create(
|
||||
{
|
||||
users: [{ userId: viewerId(), isOwner: 1 }],
|
||||
},
|
||||
{
|
||||
requests: [
|
||||
{
|
||||
users: [{ userId: requesterId(), isOwner: 1 }],
|
||||
isAccepted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const data = await loadFilteredByAssociation(association.id);
|
||||
|
||||
expect(data.posts.booked.map((post) => post.id)).toEqual([bookedPostId]);
|
||||
});
|
||||
|
||||
test("ignores a filter for an association the viewer is not in", async () => {
|
||||
const otherAssociation = await AssociationFactory.create({
|
||||
userId: otherUserId(),
|
||||
});
|
||||
|
||||
const { id: postId } = await ScrimPostFactory.create({
|
||||
users: [{ userId: otherUserId(), isOwner: 1 }],
|
||||
});
|
||||
|
||||
const data = await loadFilteredByAssociation(otherAssociation.id);
|
||||
|
||||
expect(data.associationFilter).toBeNull();
|
||||
expect(data.posts.neutral.map((post) => post.id)).toEqual([postId]);
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
? await AssociationsRepository.findByMemberUserId(user?.id)
|
||||
: null;
|
||||
|
||||
const { weekdayTimes, weekendTimes, divs, useDefaults } =
|
||||
const { weekdayTimes, weekendTimes, divs, useDefaults, associationId } =
|
||||
scrimsSearchParams.parse(request);
|
||||
const filtersFromSearchParams = { weekdayTimes, weekendTimes, divs };
|
||||
|
||||
@@ -31,26 +31,46 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
? (user?.preferences?.defaultScrimsFilters ?? Scrim.defaultFilters())
|
||||
: filtersFromSearchParams;
|
||||
|
||||
const posts = (await ScrimPostRepository.findAllRelevant())
|
||||
.filter(
|
||||
(post) =>
|
||||
(user && Scrim.isParticipating(post, user.id)) ||
|
||||
Association.isVisible({
|
||||
associations,
|
||||
visibility: post.visibility,
|
||||
contentOwnerUserId: post.users.find((u) => u.isOwner)?.id,
|
||||
}),
|
||||
)
|
||||
.map((post) => ({
|
||||
...post,
|
||||
visibility: null,
|
||||
isPrivate: !Association.isPublic({
|
||||
// a filter for an association the viewer is not in is ignored rather than showing an empty page
|
||||
const associationFilter =
|
||||
associations?.actual.find(
|
||||
(association) => association.id === associationId,
|
||||
) ?? null;
|
||||
|
||||
const visiblePosts = (await ScrimPostRepository.findAllRelevant()).filter(
|
||||
(post) =>
|
||||
(user && Scrim.isParticipating(post, user.id)) ||
|
||||
Association.isVisible({
|
||||
associations,
|
||||
visibility: post.visibility,
|
||||
contentOwnerUserId: post.users.find((u) => u.isOwner)?.id,
|
||||
}),
|
||||
}));
|
||||
);
|
||||
|
||||
const divided = dividePosts(visiblePosts, user?.id);
|
||||
|
||||
// the association filter narrows browsing only, the viewer's own and booked posts stay listed
|
||||
const dividedPosts = {
|
||||
neutral: divided.neutral
|
||||
.filter(
|
||||
(post) =>
|
||||
!associationFilter ||
|
||||
Association.mentionsAssociation({
|
||||
visibility: post.visibility,
|
||||
associationId: associationFilter.id,
|
||||
}),
|
||||
)
|
||||
.map(censorVisibility),
|
||||
owned: divided.owned.map(censorVisibility),
|
||||
booked: divided.booked.map(censorVisibility),
|
||||
};
|
||||
|
||||
const cardUserIds = R.unique(
|
||||
posts.flatMap((post) => [
|
||||
[
|
||||
...dividedPosts.neutral,
|
||||
...dividedPosts.owned,
|
||||
...dividedPosts.booked,
|
||||
].flatMap((post) => [
|
||||
...post.users.map((postUser) => postUser.id),
|
||||
...post.requests.flatMap((postRequest) =>
|
||||
postRequest.users.map((requestUser) => requestUser.id),
|
||||
@@ -58,7 +78,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
]),
|
||||
);
|
||||
|
||||
const dividedPosts = dividePosts(posts, user?.id);
|
||||
const teams = user ? await TeamRepository.findAllByMemberUserId(user.id) : [];
|
||||
|
||||
return {
|
||||
@@ -73,6 +92,14 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
viewerId: user?.id ?? null,
|
||||
}),
|
||||
filters,
|
||||
associationFilter: associationFilter
|
||||
? { id: associationFilter.id, name: associationFilter.name }
|
||||
: null,
|
||||
associationOptions:
|
||||
associations?.actual.map((association) => ({
|
||||
id: association.id,
|
||||
name: association.name,
|
||||
})) ?? [],
|
||||
canSaveAsDefault:
|
||||
user != null &&
|
||||
!R.isDeepEqual(
|
||||
@@ -82,6 +109,17 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
};
|
||||
};
|
||||
|
||||
/** Replaces the raw visibility with a flag, so that association details do not reach the client. */
|
||||
function censorVisibility(post: ScrimPost): ScrimPost {
|
||||
return {
|
||||
...post,
|
||||
visibility: null,
|
||||
isPrivate: !Association.isPublic({
|
||||
visibility: post.visibility,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** How the viewer's teams relate to the requestable posts, one entry per post: what the fit indicators on cards and in the request dialog resolve from. */
|
||||
async function rosterAvailability({
|
||||
posts,
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as R from "remeda";
|
||||
import * as v from "valibot";
|
||||
import { EmptyState } from "~/components/EmptyState";
|
||||
import { LinkButton, SendouButton } from "~/components/elements/Button";
|
||||
import { SendouSelect, SendouSelectItem } from "~/components/elements/Select";
|
||||
import { FilterBar } from "~/components/filter-bar/FilterBar";
|
||||
import { LocaleTime } from "~/components/LocaleTime";
|
||||
import { associationsPage } from "~/features/associations/associations-urls";
|
||||
@@ -197,6 +198,35 @@ function Filters() {
|
||||
setParams({ ...filters, ...partial, useDefaults: false });
|
||||
};
|
||||
|
||||
const associationPill =
|
||||
data.associationOptions.length > 0
|
||||
? [
|
||||
{
|
||||
key: "association",
|
||||
name: t("scrims:filters.association"),
|
||||
formattedValue: data.associationFilter?.name ?? null,
|
||||
onRemove: () => setParams({ associationId: null }),
|
||||
testId: "association-filter",
|
||||
popover: (
|
||||
<SendouSelect
|
||||
aria-label={t("scrims:filters.association")}
|
||||
items={data.associationOptions}
|
||||
selectedKey={data.associationFilter?.id ?? null}
|
||||
onSelectionChange={(key) =>
|
||||
setParams({ associationId: key as number | null })
|
||||
}
|
||||
>
|
||||
{({ id, name }) => (
|
||||
<SendouSelectItem key={id} id={id}>
|
||||
{name}
|
||||
</SendouSelectItem>
|
||||
)}
|
||||
</SendouSelect>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<FilterBar
|
||||
pills={[
|
||||
@@ -251,14 +281,17 @@ function Filters() {
|
||||
/>
|
||||
),
|
||||
},
|
||||
...associationPill,
|
||||
]}
|
||||
onReset={
|
||||
!Scrim.filtersAreDefault(filters)
|
||||
!Scrim.filtersAreDefault(filters) || data.associationFilter
|
||||
? () =>
|
||||
writeFilters({
|
||||
setParams({
|
||||
weekdayTimes: null,
|
||||
weekendTimes: null,
|
||||
divs: null,
|
||||
associationId: null,
|
||||
useDefaults: false,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ describe("scrimsSearchParams", () => {
|
||||
],
|
||||
pendingRequestPostId: [null, 1, 987654],
|
||||
useDefaults: [true, false],
|
||||
associationId: [null, 1, 42],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,5 +50,11 @@ describe("scrimsSearchParams", () => {
|
||||
["-2"],
|
||||
["1.5"],
|
||||
]);
|
||||
assertDecodesToDefault(scrimsSearchParams, "associationId", [
|
||||
["abc"],
|
||||
["0"],
|
||||
["-2"],
|
||||
["1.5"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,11 @@ export const scrimsSearchParams = SearchParams.define({
|
||||
divs: SP.custom(divsCodec, { loader: true }),
|
||||
/** False once the user has edited the filters, making the URL win over their saved defaults. */
|
||||
useDefaults: SP.param(v.boolean(), { default: true, loader: true }),
|
||||
/** Only posts whose visibility ever mentioned this association, not part of the saved default filters. */
|
||||
associationId: SP.param(
|
||||
v.nullable(v.pipe(v.number(), v.integer(), v.gtValue(0))),
|
||||
{ loader: true },
|
||||
),
|
||||
pendingRequestPostId: SP.param(
|
||||
v.nullable(v.pipe(v.number(), v.integer(), v.gtValue(0))),
|
||||
{
|
||||
|
||||
5
app/features/scrims/scrims-urls.ts
Normal file
5
app/features/scrims/scrims-urls.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { scrimsPage } from "~/utils/urls";
|
||||
import { scrimsSearchParams } from "./scrims-search-params";
|
||||
|
||||
export const scrimsByAssociationPage = (associationId: number) =>
|
||||
scrimsSearchParams.href(scrimsPage(), { associationId });
|
||||
5
changelog/2026-09-20-scrims-association-filter.md
Normal file
5
changelog/2026-09-20-scrims-association-filter.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
navItem: scrims
|
||||
type: feature
|
||||
---
|
||||
Scrims can be filtered by association, with a "View scrims" shortcut on the associations page
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "",
|
||||
"associations.delete.title": "",
|
||||
"associations.admin": "",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "Assoziation {{name}} beitreten?",
|
||||
"associations.delete.title": "Assoziation {{name}} löschen?",
|
||||
"associations.admin": "Admin: {{username}}",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "Assoziation {{name}} verlassen?",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "Verlassen",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "Weekday times",
|
||||
"filters.weekendTimes": "Weekend times",
|
||||
"filters.divs": "Divs",
|
||||
"filters.association": "Association",
|
||||
"filters.showFiltered": "Show filtered ({{count}})",
|
||||
"filters.hideFiltered": "Hide filtered ({{count}})",
|
||||
"filters.showPendingRequests": "Show pending requests ({{count}})",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "Join {{name}} association?",
|
||||
"associations.delete.title": "Delete {{name}} association?",
|
||||
"associations.admin": "Admin: {{username}}",
|
||||
"associations.viewScrims": "View scrims",
|
||||
"associations.leave.title": "Leave {{name}} association?",
|
||||
"associations.leave.titleWithNewAdmin": "Leave {{name}} association? {{username}} will become the new admin",
|
||||
"associations.leave.action": "Leave",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "Horarios entre semana",
|
||||
"filters.weekendTimes": "Horarios de fin de semana",
|
||||
"filters.divs": "Divs",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "Mostrar filtrados ({{count}})",
|
||||
"filters.hideFiltered": "Ocultar filtrados ({{count}})",
|
||||
"filters.showPendingRequests": "Mostrar peticiones pendientes ({{count}})",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "¿Unirse a la asociación {{name}}?",
|
||||
"associations.delete.title": "¿Eliminar la asociación {{name}}?",
|
||||
"associations.admin": "Administrador: {{username}}",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "¿Salir de la asociación {{name}}?",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "Salir",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "Horarios entre semana",
|
||||
"filters.weekendTimes": "Horarios de fin de semana",
|
||||
"filters.divs": "Divs",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "Mostrar filtrados ({{count}})",
|
||||
"filters.hideFiltered": "Ocultar filtrados ({{count}})",
|
||||
"filters.showPendingRequests": "Mostrar peticiones pendientes ({{count}})",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "¿Unirse a la asociación {{name}}?",
|
||||
"associations.delete.title": "¿Eliminar la asociación {{name}}?",
|
||||
"associations.admin": "Administrador: {{username}}",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "¿Salir de la asociación {{name}}?",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "Salir",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "",
|
||||
"associations.delete.title": "",
|
||||
"associations.admin": "",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "Rejoindre l'association {{name}} ?",
|
||||
"associations.delete.title": "Supprimer l'association {{name}} ?",
|
||||
"associations.admin": "Admin: {{username}}",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "Quitter l'association {{name}} ?",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "Quitter",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "",
|
||||
"associations.delete.title": "",
|
||||
"associations.admin": "",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "",
|
||||
"associations.delete.title": "",
|
||||
"associations.admin": "",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "",
|
||||
"associations.delete.title": "",
|
||||
"associations.admin": "",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "",
|
||||
"associations.delete.title": "",
|
||||
"associations.admin": "",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "",
|
||||
"associations.delete.title": "",
|
||||
"associations.admin": "",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "",
|
||||
"associations.delete.title": "",
|
||||
"associations.admin": "",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "",
|
||||
"associations.delete.title": "",
|
||||
"associations.admin": "",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "",
|
||||
"filters.weekendTimes": "",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "",
|
||||
"filters.hideFiltered": "",
|
||||
"filters.showPendingRequests": "",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "",
|
||||
"associations.delete.title": "",
|
||||
"associations.admin": "",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filters.weekdayTimes": "工作日时间",
|
||||
"filters.weekendTimes": "周末时间",
|
||||
"filters.divs": "",
|
||||
"filters.association": "",
|
||||
"filters.showFiltered": "显示已过滤内容 ({{count}})",
|
||||
"filters.hideFiltered": "隐藏已过滤内容 ({{count}})",
|
||||
"filters.showPendingRequests": "显示待处理请求 ({{count}})",
|
||||
@@ -51,6 +52,7 @@
|
||||
"associations.join.title": "加入群组 {{name}} ?",
|
||||
"associations.delete.title": "删除群组 {{name}} ?",
|
||||
"associations.admin": "管理员: {{username}}",
|
||||
"associations.viewScrims": "",
|
||||
"associations.leave.title": "退出群组 {{name}} ?",
|
||||
"associations.leave.titleWithNewAdmin": "",
|
||||
"associations.leave.action": "退出",
|
||||
|
||||
Reference in New Issue
Block a user