add experimental feature gate

This commit is contained in:
Kaity A 2023-05-13 23:41:36 +10:00
parent 7ff5f1f72e
commit 804dbe6985
10 changed files with 150 additions and 7 deletions

View file

@ -2008,3 +2008,10 @@ _deck:
list: "List" list: "List"
mentions: "Mentions" mentions: "Mentions"
direct: "Direct messages" direct: "Direct messages"
_experiments:
title: "Experiments"
alpha: "Alpha"
beta: "Beta"
release: "Release"
enableExperimentalPostEditing: "Enable experimental post editing"
experimentalPostEditingCaption: "Enables the option for users to edit their posts in post options"

View file

@ -0,0 +1,16 @@
export class ExperimentalFeatures1683980686995 {
name = "ExperimentalFeatures1683980686995";
async up(queryRunner) {
await queryRunner.query(`
ALTER TABLE "meta"
ADD "experimentalFeatures" jsonb NOT NULL DEFAULT '{}'
`);
}
async down(queryRunner) {
await queryRunner.query(`
ALTER TABLE "meta" DROP COLUMN "experimentalFeatures"
`);
}
}

View file

@ -516,4 +516,9 @@ export class Meta {
default: true, default: true,
}) })
public enableActiveEmailValidation: boolean; public enableActiveEmailValidation: boolean;
@Column('jsonb', {
default: {},
})
public experimentalFeatures: Record<string, unknown>;
} }

View file

@ -2,6 +2,7 @@ import config from "@/config/index.js";
import { fetchMeta } from "@/misc/fetch-meta.js"; import { fetchMeta } from "@/misc/fetch-meta.js";
import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
import define from "../../define.js"; import define from "../../define.js";
import { Exp } from "@tensorflow/tfjs";
export const meta = { export const meta = {
tags: ["meta"], tags: ["meta"],
@ -470,10 +471,20 @@ export const meta = {
optional: false, optional: false,
nullable: false, nullable: false,
}, },
experimentalFeatures: {
type: "object",
optional: true,
nullable: true,
ref: "MetaExperimentalFeatures",
},
}, },
}, },
} as const; } as const;
export type MetaExperimentalFeatures = {
postEditing: boolean;
};
export const paramDef = { export const paramDef = {
type: "object", type: "object",
properties: {}, properties: {},

View file

@ -170,6 +170,13 @@ export const paramDef = {
objectStorageS3ForcePathStyle: { type: "boolean" }, objectStorageS3ForcePathStyle: { type: "boolean" },
enableIpLogging: { type: "boolean" }, enableIpLogging: { type: "boolean" },
enableActiveEmailValidation: { type: "boolean" }, enableActiveEmailValidation: { type: "boolean" },
experimentalFeatures: {
type: "object",
nullable: true,
properties: {
postEditing: { type: "boolean" },
},
},
}, },
required: [], required: [],
} as const; } as const;

View file

@ -304,6 +304,7 @@ export type LiteInstanceMetadata = {
url: string; url: string;
imageUrl: string; imageUrl: string;
}[]; }[];
experimentalFeatures?: Record<string, any>;
}; };
export type DetailedInstanceMetadata = LiteInstanceMetadata & { export type DetailedInstanceMetadata = LiteInstanceMetadata & {

View file

@ -0,0 +1,83 @@
<template>
<MkStickyContainer>
<FormSuspense :p="init">
<FormSwitch
v-model="enableExperimentalPostEditing"
@update:modelValue="save"
class="_formBlock"
>
<template #label
><i class="ph-pencil-line ph-bold ph-lg"></i
>{{ i18n.ts._experiments.enableExperimentalPostEditing
}}<span class="level alpha"
>({{ i18n.ts._experiments.alpha }})</span
></template
>
<template #caption>{{
i18n.ts._experiments.experimentalPostEditingCaption
}}</template>
</FormSwitch>
</FormSuspense>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import {} from "vue";
import MkStickyContainer from "@/components/global/MkStickyContainer.vue";
import FormSuspense from "@/components/form/suspense.vue";
import FormSwitch from "@/components/form/switch.vue";
import * as os from "@/os";
import { fetchInstance } from "@/instance";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
let enableExperimentalPostEditing = $ref(false);
let meta = $ref<MetaExperiments | null>(null);
type MetaExperiments = {
experimentalFeatures?: {
postEditing?: boolean;
};
};
async function init() {
meta = (await os.api("admin/meta")) as MetaExperiments;
if (!meta) return;
enableExperimentalPostEditing =
meta.experimentalFeatures?.postEditing ?? false;
}
function save() {
const experiments: MetaExperiments = {
experimentalFeatures: {
postEditing: enableExperimentalPostEditing,
},
};
os.apiWithDialog("admin/update-meta", experiments).then(() => {
fetchInstance();
});
}
definePageMetadata({
title: i18n.ts._experiments.title,
icon: "ph-flask ph-bold ph-lg",
});
</script>
<style lang="scss" scoped>
.level {
font-size: 0.8em;
color: var(--color-gray-500);
text-transform: uppercase;
&.alpha {
color: #f59e0b;
}
&.beta {
color: #0ea5e9;
}
&.release {
color: #10b981;
}
}
</style>

View file

@ -300,6 +300,12 @@ const menuDef = $computed(() => [
to: "/admin/database", to: "/admin/database",
active: currentPage?.route.name === "database", active: currentPage?.route.name === "database",
}, },
{
icon: "ph-flask ph-bold ph-lg",
text: i18n.ts._experiments.title,
to: "/admin/experiments",
active: currentPage?.route.name === "experiments",
},
], ],
}, },
] ]

View file

@ -540,6 +540,11 @@ export const routes = [
name: "other-settings", name: "other-settings",
component: page(() => import("./pages/admin/custom-css.vue")), component: page(() => import("./pages/admin/custom-css.vue")),
}, },
{
path: "/experiments",
name: "experiments",
component: page(() => import("./pages/admin/experiments.vue")),
},
{ {
path: "/", path: "/",
component: page(() => import("./pages/_empty_.vue")), component: page(() => import("./pages/_empty_.vue")),

View file

@ -421,7 +421,7 @@ export function getNoteMenu(props: {
null, null,
isAppearAuthor instance.experimentalFeatures?.postEditing && isAppearAuthor
? { ? {
icon: "ph-pencil-line ph-bold ph-lg", icon: "ph-pencil-line ph-bold ph-lg",
text: i18n.ts.edit, text: i18n.ts.edit,
@ -430,12 +430,14 @@ export function getNoteMenu(props: {
} }
: undefined, : undefined,
{ instance.experimentalFeatures?.postEditing
icon: "ph-copy ph-bold ph-lg", ? {
text: i18n.ts.duplicate, icon: "ph-copy ph-bold ph-lg",
textStyle: "color: var(--accent)", text: i18n.ts.duplicate,
action: duplicate, textStyle: "color: var(--accent)",
}, action: duplicate,
}
: undefined,
isAppearAuthor || isModerator isAppearAuthor || isModerator
? { ? {