mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-12 23:56:07 +00:00
feat(sub): per-inbound sort order for subscription links
Add a subSortIndex field to inbounds that controls the order of links in subscription output only: the raw sub body, the HTML sub page, and the JSON/Clash formats (all served from the same query). Lower values come first; ties keep id order. The panel inbound list is unaffected. The value is editable in the inbound form next to the share-address fields, propagates to nodes via wireInbound, and follows the usual node-sync rules (copied on import, mirrored while not dirty, never a structural change). Rescoped from #5214 by @Ponywka.
This commit is contained in:
@@ -7,6 +7,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > http
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Subscription sort order",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -22,6 +23,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > hyste
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Subscription sort order",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -37,6 +39,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > mixed
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Subscription sort order",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -52,6 +55,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > shado
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Subscription sort order",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -67,6 +71,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > troja
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Subscription sort order",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -82,6 +87,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > tun 1
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Subscription sort order",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -97,6 +103,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > tunne
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Subscription sort order",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -112,6 +119,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > vless
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Subscription sort order",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -127,6 +135,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > vmess
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Subscription sort order",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
@@ -142,6 +151,7 @@ exports[`InboundFormModal > field structure is stable for every protocol > wireg
|
||||
"Protocol",
|
||||
"Address",
|
||||
"Share address strategy",
|
||||
"Subscription sort order",
|
||||
"Port",
|
||||
"Total Flow",
|
||||
"Traffic Reset",
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
formValuesToWirePayload,
|
||||
type RawInboundRow,
|
||||
} from '@/lib/xray/inbound-form-adapter';
|
||||
import { InboundFormSchema } from '@/schemas/forms/inbound-form';
|
||||
import { InboundDbFieldsSchema, InboundFormSchema } from '@/schemas/forms/inbound-form';
|
||||
import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
|
||||
|
||||
// Round-trip: raw DB row → InboundFormValues → wire payload, asserting
|
||||
@@ -262,3 +262,35 @@ describe('formValuesToWirePayload', () => {
|
||||
expect(replay.streamSettings).toEqual(original.streamSettings);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subSortIndex', () => {
|
||||
it('rawInboundToFormValues defaults to 1 when field is absent', () => {
|
||||
const values = rawInboundToFormValues({ ...vlessRow, subSortIndex: undefined });
|
||||
expect(values.subSortIndex).toBe(1);
|
||||
});
|
||||
|
||||
it('rawInboundToFormValues preserves valid values and clamps below-minimum ones to 1', () => {
|
||||
expect(rawInboundToFormValues({ ...vlessRow, subSortIndex: 5 }).subSortIndex).toBe(5);
|
||||
expect(rawInboundToFormValues({ ...vlessRow, subSortIndex: 0 }).subSortIndex).toBe(1);
|
||||
expect(rawInboundToFormValues({ ...vlessRow, subSortIndex: -10 }).subSortIndex).toBe(1);
|
||||
});
|
||||
|
||||
it('formValuesToWirePayload includes subSortIndex in the payload', () => {
|
||||
const values = rawInboundToFormValues({ ...vlessRow, subSortIndex: 3 });
|
||||
const payload = formValuesToWirePayload(values);
|
||||
expect(payload.subSortIndex).toBe(3);
|
||||
});
|
||||
|
||||
it('subSortIndex round-trips through raw → values → payload', () => {
|
||||
const values = rawInboundToFormValues({ ...vlessRow, subSortIndex: 42 });
|
||||
const payload = formValuesToWirePayload(values);
|
||||
const replay = rawInboundToFormValues({ ...vlessRow, subSortIndex: payload.subSortIndex });
|
||||
expect(replay.subSortIndex).toBe(42);
|
||||
});
|
||||
|
||||
it('InboundDbFieldsSchema enforces an integer minimum of 1 and defaults to 1', () => {
|
||||
expect(InboundDbFieldsSchema.partial().safeParse({ subSortIndex: 1.5 }).success).toBe(false);
|
||||
expect(InboundDbFieldsSchema.partial().safeParse({ subSortIndex: 0 }).success).toBe(false);
|
||||
expect(InboundDbFieldsSchema.parse({}).subSortIndex).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user