Merge pull request 'clan-app: Remove unused files and exports from ui-2d' (#4027) from Qubasa/clan-core:ui_minimize into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4027
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
"stylelint.config.js",
|
||||
"util.ts",
|
||||
"src/components/v2/**",
|
||||
"api/**"
|
||||
"api/**",
|
||||
"tailwind/**"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,37 +17,30 @@
|
||||
"storybook-dev": "storybook dev -p 6006",
|
||||
"test-storybook": "vitest run --project storybook",
|
||||
"test-storybook-update-snapshots": "vitest run --project storybook --update",
|
||||
"test-storybook-static": "npm run storybook-build && concurrently -k -s first -n 'SB,TEST' -c 'magenta,blue' 'http-server storybook-static --port 6006 --silent' 'wait-on tcp:127.0.0.1:6006 && npm run test-storybook'"
|
||||
"test-storybook-static": "npm run storybook-build && concurrently -k -s first -n 'SB,TEST' -c 'magenta,blue' 'npx http-server storybook-static --port 6006 --silent' 'npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook'"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@kachurun/storybook-solid": "^9.0.11",
|
||||
"@kachurun/storybook-solid-vite": "^9.0.11",
|
||||
"@storybook/addon-a11y": "^9.0.8",
|
||||
"@storybook/addon-docs": "^9.0.8",
|
||||
"@storybook/addon-links": "^9.0.8",
|
||||
"@storybook/addon-onboarding": "^9.0.8",
|
||||
"@storybook/addon-viewport": "^9.0.8",
|
||||
"@storybook/addon-vitest": "^9.0.8",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/node": "^22.15.19",
|
||||
"@types/three": "^0.177.0",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@vitest/browser": "^3.2.3",
|
||||
"@vitest/coverage-v8": "^3.2.3",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"classnames": "^2.5.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"knip": "^5.61.2",
|
||||
"playwright": "~1.53.0",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-url": "^10.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
"solid-devtools": "^0.34.0",
|
||||
"storybook": "^9.0.8",
|
||||
@@ -57,8 +50,7 @@
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-solid": "^2.8.2",
|
||||
"vite-plugin-solid-svg": "^0.8.1",
|
||||
"vitest": "^3.2.3",
|
||||
"wait-on": "^8.0.3"
|
||||
"vitest": "^3.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.8",
|
||||
@@ -70,12 +62,9 @@
|
||||
"@tanstack/eslint-plugin-query": "^5.51.12",
|
||||
"@tanstack/solid-query": "^5.76.0",
|
||||
"corvu": "^0.7.1",
|
||||
"material-icons": "^1.13.12",
|
||||
"nanoid": "^5.0.7",
|
||||
"solid-js": "^1.9.7",
|
||||
"solid-markdown": "^2.0.13",
|
||||
"solid-toast": "^0.5.0",
|
||||
"three": "^0.177.0"
|
||||
"solid-toast": "^0.5.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-arm64": "^0.25.4",
|
||||
|
||||
@@ -6,10 +6,8 @@ import type {
|
||||
} from "@floating-ui/dom";
|
||||
import { computePosition } from "@floating-ui/dom";
|
||||
|
||||
export interface UseFloatingOptions<
|
||||
R extends ReferenceElement,
|
||||
F extends HTMLElement,
|
||||
> extends Partial<ComputePositionConfig> {
|
||||
interface UseFloatingOptions<R extends ReferenceElement, F extends HTMLElement>
|
||||
extends Partial<ComputePositionConfig> {
|
||||
whileElementsMounted?: (
|
||||
reference: R,
|
||||
floating: F,
|
||||
@@ -23,7 +21,7 @@ interface UseFloatingState extends Omit<ComputePositionReturn, "x" | "y"> {
|
||||
y?: number | null;
|
||||
}
|
||||
|
||||
export interface UseFloatingResult extends UseFloatingState {
|
||||
interface UseFloatingResult extends UseFloatingState {
|
||||
update(): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@ import { JSX } from "solid-js";
|
||||
interface FormSectionProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
export const FormSection = (props: FormSectionProps) => {
|
||||
const FormSection = (props: FormSectionProps) => {
|
||||
return <div class="p-2">{props.children}</div>;
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import { FieldLayout } from "./layout";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { useContext } from "corvu/dialog";
|
||||
|
||||
export interface Option {
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -139,7 +139,7 @@ interface SchemaFieldsProps<T extends FieldValues, R extends ResponseData> {
|
||||
readonly: boolean;
|
||||
parent: JSONSchema7;
|
||||
}
|
||||
export function SchemaFields<T extends FieldValues, R extends ResponseData>(
|
||||
function SchemaFields<T extends FieldValues, R extends ResponseData>(
|
||||
props: SchemaFieldsProps<T, R>,
|
||||
) {
|
||||
return (
|
||||
@@ -172,7 +172,7 @@ export function SchemaFields<T extends FieldValues, R extends ResponseData>(
|
||||
);
|
||||
}
|
||||
|
||||
export function StringField<T extends FieldValues, R extends ResponseData>(
|
||||
function StringField<T extends FieldValues, R extends ResponseData>(
|
||||
props: SchemaFieldsProps<T, R>,
|
||||
) {
|
||||
if (
|
||||
@@ -325,7 +325,7 @@ export function StringField<T extends FieldValues, R extends ResponseData>(
|
||||
interface OptionSchemaProps {
|
||||
itemSpec: JSONSchema7Type;
|
||||
}
|
||||
export function OptionSchema(props: OptionSchemaProps) {
|
||||
function OptionSchema(props: OptionSchemaProps) {
|
||||
return (
|
||||
<Switch
|
||||
fallback={<option class="text-error-700">Item spec unhandled</option>}
|
||||
@@ -344,7 +344,7 @@ interface ValueDisplayProps<T extends FieldValues, R extends ResponseData>
|
||||
idx: number;
|
||||
of: number;
|
||||
}
|
||||
export function ListValueDisplay<T extends FieldValues, R extends ResponseData>(
|
||||
function ListValueDisplay<T extends FieldValues, R extends ResponseData>(
|
||||
props: ValueDisplayProps<T, R>,
|
||||
) {
|
||||
const removeItem = (e: Event) => {
|
||||
@@ -446,7 +446,7 @@ const OnlyStringItems = (props: OnlyStringItems) => {
|
||||
);
|
||||
};
|
||||
|
||||
export function ArrayFields<T extends FieldValues, R extends ResponseData>(
|
||||
function ArrayFields<T extends FieldValues, R extends ResponseData>(
|
||||
props: SchemaFieldsProps<T, R>,
|
||||
) {
|
||||
if (props.schema.type !== "array") {
|
||||
@@ -711,7 +711,7 @@ interface ObjectFieldPropertyLabelProps {
|
||||
schema: JSONSchema7;
|
||||
fallback: JSX.Element;
|
||||
}
|
||||
export function ObjectFieldPropertyLabel(props: ObjectFieldPropertyLabelProps) {
|
||||
function ObjectFieldPropertyLabel(props: ObjectFieldPropertyLabelProps) {
|
||||
return (
|
||||
<Switch fallback={props.fallback}>
|
||||
{/* @ts-expect-error: $exportedModuleInfo should exist since we export it */}
|
||||
@@ -722,7 +722,7 @@ export function ObjectFieldPropertyLabel(props: ObjectFieldPropertyLabelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ObjectFields<T extends FieldValues, R extends ResponseData>(
|
||||
function ObjectFields<T extends FieldValues, R extends ResponseData>(
|
||||
props: SchemaFieldsProps<T, R>,
|
||||
) {
|
||||
if (props.schema.type !== "object") {
|
||||
|
||||
@@ -7,21 +7,14 @@ import {
|
||||
ErrorToastComponent,
|
||||
CancelToastComponent,
|
||||
} from "@/src/components/toast";
|
||||
export type OperationNames = keyof API;
|
||||
|
||||
type OperationNames = keyof API;
|
||||
type Services = NonNullable<Inventory["services"]>;
|
||||
type ServiceNames = keyof Services;
|
||||
|
||||
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
|
||||
export type OperationResponse<T extends OperationNames> = API[T]["return"];
|
||||
|
||||
export type ApiEnvelope<T> =
|
||||
| {
|
||||
status: "success";
|
||||
data: T;
|
||||
op_key: string;
|
||||
}
|
||||
| ApiError;
|
||||
|
||||
export type Services = NonNullable<Inventory["services"]>;
|
||||
export type ServiceNames = keyof Services;
|
||||
export type ClanService<T extends ServiceNames> = Services[T];
|
||||
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
|
||||
Services[T]
|
||||
>[string];
|
||||
@@ -32,18 +25,6 @@ export type SuccessQuery<T extends OperationNames> = Extract<
|
||||
>;
|
||||
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
|
||||
|
||||
export type ErrorQuery<T extends OperationNames> = Extract<
|
||||
OperationResponse<T>,
|
||||
{ status: "error" }
|
||||
>;
|
||||
export type ErrorData<T extends OperationNames> = ErrorQuery<T>["errors"];
|
||||
|
||||
export type ClanOperations = Record<OperationNames, (str: string) => void>;
|
||||
|
||||
export interface GtkResponse<T> {
|
||||
result: T;
|
||||
op_key: string;
|
||||
}
|
||||
const _callApi = <K extends OperationNames>(
|
||||
method: K,
|
||||
args: OperationArgs<K>,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { callApi } from ".";
|
||||
import { Schema as Inventory } from "@/api/Inventory";
|
||||
|
||||
export const instance_name = (machine_name: string) =>
|
||||
`${machine_name}_wifi_0` as const;
|
||||
|
||||
export async function get_iwd_service(base_path: string, machine_name: string) {
|
||||
const r = await callApi("get_inventory", {
|
||||
flake: { identifier: base_path },
|
||||
}).promise;
|
||||
if (r.status == "error") {
|
||||
return null;
|
||||
}
|
||||
// @FIXME: Clean this up once we implement the feature
|
||||
// @ts-expect-error: This doesn't check currently
|
||||
const inventory: Inventory = r.data;
|
||||
|
||||
const instance_key = instance_name(machine_name);
|
||||
return inventory.services?.iwd?.[instance_key] || null;
|
||||
}
|
||||
@@ -42,8 +42,7 @@ const sizeFont: Record<Size, string> = {
|
||||
s: cx("text-[0.75rem]"),
|
||||
};
|
||||
|
||||
export interface ButtonProps
|
||||
extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: Variants;
|
||||
size?: Size;
|
||||
children?: JSX.Element;
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { FieldValues, FormStore, ResponseData } from "@modular-forms/solid";
|
||||
import { Show } from "solid-js";
|
||||
import { type JSX } from "solid-js";
|
||||
import cx from "classnames";
|
||||
|
||||
interface SelectInputProps<T extends FieldValues, R extends ResponseData> {
|
||||
formStore: FormStore<T, R>;
|
||||
value: string;
|
||||
options: JSX.Element;
|
||||
selectProps: JSX.HTMLAttributes<HTMLSelectElement>;
|
||||
label: JSX.Element;
|
||||
error?: string;
|
||||
required?: boolean;
|
||||
topRightLabel?: JSX.Element;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function SelectInput<T extends FieldValues, R extends ResponseData>(
|
||||
props: SelectInputProps<T, R>,
|
||||
) {
|
||||
return (
|
||||
<label
|
||||
class={cx(" w-full", props.class)}
|
||||
aria-disabled={props.formStore.submitting}
|
||||
>
|
||||
<div class="">
|
||||
<span
|
||||
class=" block"
|
||||
classList={{
|
||||
"after:ml-0.5 after:text-primary after:content-['*']":
|
||||
props.required,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
</span>
|
||||
<Show when={props.topRightLabel}>
|
||||
<span class="">{props.topRightLabel}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<select
|
||||
{...props.selectProps}
|
||||
required={props.required}
|
||||
class="w-full"
|
||||
value={props.value}
|
||||
>
|
||||
{props.options}
|
||||
</select>
|
||||
|
||||
{props.error && (
|
||||
<span class=" font-bold text-error-700">{props.error}</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import "./css/sidebar.css";
|
||||
import Icon, { IconVariant } from "../icon";
|
||||
import { clanMetaQuery } from "@/src/queries/clan-meta";
|
||||
|
||||
export const SidebarSection = (props: {
|
||||
const SidebarSection = (props: {
|
||||
title: string;
|
||||
icon: IconVariant;
|
||||
children: JSX.Element;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, For } from "solid-js";
|
||||
import { Typography } from "@/src/components/Typography";
|
||||
import "./TagList.css";
|
||||
|
||||
export interface TagListProps {
|
||||
interface TagListProps {
|
||||
values: string[];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Dynamic } from "solid-js/web";
|
||||
import cx from "classnames";
|
||||
import "./css/typography.css";
|
||||
|
||||
export type Hierarchy = "body" | "title" | "headline" | "label";
|
||||
type Hierarchy = "body" | "title" | "headline" | "label";
|
||||
type Color = "primary" | "secondary" | "tertiary";
|
||||
type Weight = "normal" | "medium" | "bold";
|
||||
type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "div";
|
||||
|
||||
@@ -12,7 +12,7 @@ export const Group = (props: GroupProps) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
export type SectionVariant = "attention" | "danger";
|
||||
type SectionVariant = "attention" | "danger";
|
||||
|
||||
interface SectionHeaderProps {
|
||||
variant: SectionVariant;
|
||||
|
||||
@@ -108,7 +108,7 @@ export const RndThumbnail = (props: RndThumbnailProps) => {
|
||||
return <img src={imageSrc()} alt={props.name} />;
|
||||
};
|
||||
|
||||
export const RndThumbnailShow = () => {
|
||||
const RndThumbnailShow = () => {
|
||||
const names = ["hsjobeki", "mic92", "lassulus", "D", "A", "D", "B", "C"];
|
||||
|
||||
return (
|
||||
|
||||
@@ -68,7 +68,7 @@ const WarningIcon: Component = () => (
|
||||
|
||||
// --- Base Props and Styles ---
|
||||
|
||||
export interface BaseToastProps {
|
||||
interface BaseToastProps {
|
||||
t: Toast;
|
||||
message: string;
|
||||
onCancel?: () => void; // Optional custom function on X click
|
||||
@@ -254,7 +254,7 @@ export const CancelToastComponent: Component<BaseToastProps> = (props) => {
|
||||
};
|
||||
|
||||
// Warning Toast
|
||||
export const WarningToastComponent: Component<BaseToastProps> = (props) => {
|
||||
const WarningToastComponent: Component<BaseToastProps> = (props) => {
|
||||
let timeoutId: number | undefined;
|
||||
const [clicked, setClicked] = createSignal(false);
|
||||
const [exiting, setExiting] = createSignal(false);
|
||||
|
||||
@@ -6,10 +6,8 @@ import type {
|
||||
} from "@floating-ui/dom";
|
||||
import { computePosition } from "@floating-ui/dom";
|
||||
|
||||
export interface UseFloatingOptions<
|
||||
R extends ReferenceElement,
|
||||
F extends HTMLElement,
|
||||
> extends Partial<ComputePositionConfig> {
|
||||
interface UseFloatingOptions<R extends ReferenceElement, F extends HTMLElement>
|
||||
extends Partial<ComputePositionConfig> {
|
||||
whileElementsMounted?: (
|
||||
reference: R,
|
||||
floating: F,
|
||||
@@ -23,7 +21,7 @@ interface UseFloatingState extends Omit<ComputePositionReturn, "x" | "y"> {
|
||||
y?: number | null;
|
||||
}
|
||||
|
||||
export interface UseFloatingResult extends UseFloatingState {
|
||||
interface UseFloatingResult extends UseFloatingState {
|
||||
update(): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const registerClan = async () => {
|
||||
* Opens the custom file dialog
|
||||
* Returns a native FileList to allow interaction with the native input type="file"
|
||||
*/
|
||||
export const selectSshKeys = async (): Promise<FileList> => {
|
||||
const selectSshKeys = async (): Promise<FileList> => {
|
||||
const dataTransfer = new DataTransfer();
|
||||
|
||||
const response = await callApi("open_file", {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/solid-query";
|
||||
import { callApi } from "../api";
|
||||
import toast from "solid-toast";
|
||||
|
||||
export interface ModulesFilter {
|
||||
interface ModulesFilter {
|
||||
features: string[];
|
||||
}
|
||||
export const createModulesQuery = (
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export const colors = () => {
|
||||
return (
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="h-10 w-20 bg-red-500">red</div>
|
||||
<div class="h-10 w-20 bg-green-500">green</div>
|
||||
<div class="h-10 w-20 bg-blue-500">blue</div>
|
||||
<div class="h-10 w-20 bg-yellow-500">yellow</div>
|
||||
<div class="h-10 w-20 bg-purple-500">purple</div>
|
||||
<div class="h-10 w-20 bg-cyan-500">cyan</div>
|
||||
<div class="h-10 w-20 bg-pink-500">pink</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { callApi } from "@/src/api";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
|
||||
export const Deploy = () => {
|
||||
return <div>Deloy view</div>;
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { callApi } from "@/src/api";
|
||||
import { useQuery } from "@tanstack/solid-query";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
export function DiskView() {
|
||||
const { activeClanURI } = useClanContext();
|
||||
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ["disk", activeClanURI()],
|
||||
queryFn: async () => {
|
||||
const currUri = activeClanURI();
|
||||
if (currUri) {
|
||||
// Example of calling an API
|
||||
const result = await callApi("get_inventory", {
|
||||
flake: { identifier: currUri },
|
||||
}).promise;
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
}
|
||||
},
|
||||
}));
|
||||
return (
|
||||
<div>
|
||||
<h1>Configure Disk</h1>
|
||||
<p>
|
||||
Select machine then configure the disk. Required before installing for
|
||||
the first time.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ interface Wifi extends FieldValues {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface FlashFormValues extends FieldValues {
|
||||
interface FlashFormValues extends FieldValues {
|
||||
machine: {
|
||||
devicePath: string;
|
||||
flake: string;
|
||||
|
||||
@@ -1,9 +1,2 @@
|
||||
export { MachineActionsBar } from "./MachineActionsBar";
|
||||
export { MachineAvatar } from "./MachineAvatar";
|
||||
export { MachineForm } from "./MachineForm";
|
||||
export { MachineGeneralFields } from "./MachineGeneralFields";
|
||||
export { MachineHardwareInfo } from "./MachineHardwareInfo";
|
||||
export { InstallMachine } from "./InstallMachine";
|
||||
export { InstallProgress } from "./InstallProgress";
|
||||
export { InstallStepper } from "./InstallStepper";
|
||||
export { InstallStepNavigation } from "./InstallStepNavigation";
|
||||
export { MachineAvatar } from "./MachineAvatar";
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
export type VarsValues = FieldValues & Record<string, Record<string, string>>;
|
||||
|
||||
export interface VarsFormProps {
|
||||
interface VarsFormProps {
|
||||
machine_id: string;
|
||||
dir: string;
|
||||
handleSubmit: SubmitHandler<VarsValues>;
|
||||
@@ -27,7 +27,7 @@ export interface VarsFormProps {
|
||||
footer: JSX.Element;
|
||||
}
|
||||
|
||||
export const VarsForm = (props: VarsFormProps) => {
|
||||
const VarsForm = (props: VarsFormProps) => {
|
||||
const [formStore, { Form, Field }] = createForm<VarsValues>({});
|
||||
|
||||
const handleSubmit: SubmitHandler<VarsValues> = async (values, event) => {
|
||||
|
||||
@@ -32,7 +32,7 @@ interface AddModuleProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const AddModule = (props: AddModuleProps) => {
|
||||
const AddModule = (props: AddModuleProps) => {
|
||||
const { activeClanURI } = useClanContext();
|
||||
const tags = tagsQuery(activeClanURI());
|
||||
const machines = machinesQuery(activeClanURI());
|
||||
|
||||
@@ -140,7 +140,7 @@ interface SchemaFormProps {
|
||||
path: string[];
|
||||
}
|
||||
|
||||
export const ModuleForm = (props: { id: string }) => {
|
||||
const ModuleForm = (props: { id: string }) => {
|
||||
// TODO: Fetch the synced schema for all the modules at runtime
|
||||
// We use static schema file at build time for now. (Different versions might have different schema at runtime)
|
||||
const schemaQuery = createQuery(() => ({
|
||||
|
||||
@@ -80,7 +80,6 @@ const removeClanURI = (uri: string) => {
|
||||
|
||||
export {
|
||||
store,
|
||||
setStore,
|
||||
activeClanURI,
|
||||
setActiveClanURI,
|
||||
clanURIs,
|
||||
|
||||
Reference in New Issue
Block a user