@synopackageland/templates 0.0.0-stage → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/package.json +11 -4
  4. package/templates/_shared/AGENTS.md +149 -0
  5. package/templates/_shared/CLAUDE.md +1 -0
  6. package/templates/_shared/GO-LICENSE +27 -0
  7. package/templates/_shared/GO-PATENTS +22 -0
  8. package/templates/_shared/LICENSES/TEMPLATE.txt +21 -0
  9. package/templates/_shared/third-party-notices.mjs +55 -0
  10. package/templates/binary/LICENSES/GO-LICENSE +27 -0
  11. package/templates/binary/LICENSES/GO-PATENTS +22 -0
  12. package/templates/binary/dsm-package.yaml +46 -0
  13. package/templates/binary/example/main.go +16 -0
  14. package/templates/binary/gitignore +2 -0
  15. package/templates/binary/i18n/messages/de.json +4 -0
  16. package/templates/binary/i18n/messages/en.json +4 -0
  17. package/templates/binary/i18n/messages/es.json +4 -0
  18. package/templates/binary/i18n/messages/fr.json +4 -0
  19. package/templates/binary/i18n/messages/it.json +4 -0
  20. package/templates/binary/i18n/messages/ja.json +4 -0
  21. package/templates/binary/i18n/messages/ko.json +4 -0
  22. package/templates/binary/i18n/messages/zh-CN.json +4 -0
  23. package/templates/binary/i18n/messages/zh-TW.json +4 -0
  24. package/templates/binary/ui/app.css +4 -0
  25. package/templates/binary/ui/app.js +32 -0
  26. package/templates/compose/compose.yaml +8 -0
  27. package/templates/compose/synology-app.yaml +16 -0
  28. package/templates/native-go/README.md +372 -0
  29. package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +53 -0
  30. package/templates/native-go/backend/cmd/app/main.go +101 -0
  31. package/templates/native-go/backend/cmd/background-worker/main.go +38 -0
  32. package/templates/native-go/backend/go.mod +9 -0
  33. package/templates/native-go/backend/go.sum +4 -0
  34. package/templates/native-go/backend/internal/api/permissions.go +26 -0
  35. package/templates/native-go/backend/internal/api/permissions.json +5 -0
  36. package/templates/native-go/backend/internal/api/permissions_test.go +30 -0
  37. package/templates/native-go/backend/internal/app/background.go +249 -0
  38. package/templates/native-go/backend/internal/app/background_none.go +8 -0
  39. package/templates/native-go/backend/internal/app/background_owner.go +38 -0
  40. package/templates/native-go/backend/internal/app/background_owner_test.go +163 -0
  41. package/templates/native-go/backend/internal/app/background_schedule.go +69 -0
  42. package/templates/native-go/backend/internal/app/background_schedule_test.go +62 -0
  43. package/templates/native-go/backend/internal/app/background_test.go +320 -0
  44. package/templates/native-go/backend/internal/app/delegate_config.go +57 -0
  45. package/templates/native-go/backend/internal/app/delegate_config_test.go +36 -0
  46. package/templates/native-go/backend/internal/app/delegate_none.go +5 -0
  47. package/templates/native-go/backend/internal/app/identity.go +20 -0
  48. package/templates/native-go/backend/internal/app/identity_test.go +15 -0
  49. package/templates/native-go/backend/internal/app/migrations.go +41 -0
  50. package/templates/native-go/backend/internal/app/migrations_test.go +41 -0
  51. package/templates/native-go/backend/internal/app/portal.go +30 -0
  52. package/templates/native-go/backend/internal/app/portal_test.go +55 -0
  53. package/templates/native-go/backend/internal/app/root_none.go +9 -0
  54. package/templates/native-go/backend/internal/app/routes.go +137 -0
  55. package/templates/native-go/backend/internal/app/routes_test.go +207 -0
  56. package/templates/native-go/backend/internal/app/session_files.go +9 -0
  57. package/templates/native-go/backend/internal/app/session_files_test.go +14 -0
  58. package/templates/native-go/backend/internal/app/shared_folder.go +144 -0
  59. package/templates/native-go/backend/internal/app/shared_folder_test.go +158 -0
  60. package/templates/native-go/dsm-package.yaml +71 -0
  61. package/templates/native-go/gitignore +1 -0
  62. package/templates/native-go/i18n/glossary.json +663 -0
  63. package/templates/native-go/i18n/messages/de.json +29 -0
  64. package/templates/native-go/i18n/messages/en.json +29 -0
  65. package/templates/native-go/i18n/messages/es.json +29 -0
  66. package/templates/native-go/i18n/messages/fr.json +29 -0
  67. package/templates/native-go/i18n/messages/it.json +29 -0
  68. package/templates/native-go/i18n/messages/ja.json +29 -0
  69. package/templates/native-go/i18n/messages/ko.json +29 -0
  70. package/templates/native-go/i18n/messages/zh-CN.json +29 -0
  71. package/templates/native-go/i18n/messages/zh-TW.json +29 -0
  72. package/templates/native-go/ui/app.css +45 -0
  73. package/templates/native-go/ui/app.js +111 -0
  74. package/templates/native-go-delegate/backend/cmd/background-worker/main.go +49 -0
  75. package/templates/native-go-delegate/backend/internal/app/delegate.go +25 -0
  76. package/templates/native-go-delegate/backend/internal/app/delegate_on.go +5 -0
  77. package/templates/native-go-delegate/backend/internal/app/delegate_routes_test.go +31 -0
  78. package/templates/native-go-delegate/frontend/src/CapabilityPanels.vue +32 -0
  79. package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +77 -0
  80. package/templates/native-go-jobs/backend/internal/app/background_on.go +7 -0
  81. package/templates/native-go-jobs/backend/internal/app/background_routes_test.go +87 -0
  82. package/templates/native-go-jobs/frontend/test/background-page.test.ts +20 -0
  83. package/templates/native-go-root-ops/backend/internal/app/executor.go +128 -0
  84. package/templates/native-go-root-ops/backend/internal/app/executor_test.go +237 -0
  85. package/templates/native-go-root-ops/root/whoami.sh +3 -0
  86. package/templates/native-go-vue/README.md +51 -0
  87. package/templates/native-go-vue/frontend/DESIGN.md +61 -0
  88. package/templates/native-go-vue/frontend/package-lock.json +2550 -0
  89. package/templates/native-go-vue/frontend/package.json +23 -0
  90. package/templates/native-go-vue/frontend/src/App.vue +90 -0
  91. package/templates/native-go-vue/frontend/src/CapabilityPanels.vue +6 -0
  92. package/templates/native-go-vue/frontend/src/i18n/de.json +20 -0
  93. package/templates/native-go-vue/frontend/src/i18n/en.json +20 -0
  94. package/templates/native-go-vue/frontend/src/i18n/es.json +20 -0
  95. package/templates/native-go-vue/frontend/src/i18n/fr.json +20 -0
  96. package/templates/native-go-vue/frontend/src/i18n/it.json +20 -0
  97. package/templates/native-go-vue/frontend/src/i18n/ja.json +20 -0
  98. package/templates/native-go-vue/frontend/src/i18n/ko.json +20 -0
  99. package/templates/native-go-vue/frontend/src/i18n/zh-CN.json +20 -0
  100. package/templates/native-go-vue/frontend/src/i18n/zh-TW.json +20 -0
  101. package/templates/native-go-vue/frontend/src/i18n.ts +13 -0
  102. package/templates/native-go-vue/frontend/src/main.ts +33 -0
  103. package/templates/native-go-vue/frontend/src/style.css +27 -0
  104. package/templates/native-go-vue/frontend/test/apps.test.ts +57 -0
  105. package/templates/native-go-vue/frontend/test/native-controls.test.ts +27 -0
  106. package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +50 -0
  107. package/templates/native-go-vue/frontend/tsconfig.json +1 -0
  108. package/templates/native-go-vue/frontend/vite.config.ts +11 -0
  109. package/templates/native-go-vue/gitignore +2 -0
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "native-dsm-vue-app",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "vue-tsc --noEmit && vite build",
7
+ "test": "vitest run"
8
+ },
9
+ "dependencies": {
10
+ "@lucide/vue": "1.48.0",
11
+ "@synopackageland/sdk": "0.9.1",
12
+ "@synopackageland/ui": "0.9.0",
13
+ "vue": "3.5.43"
14
+ },
15
+ "devDependencies": {
16
+ "@vitejs/plugin-vue": "6.0.9",
17
+ "vite": "7.3.6",
18
+ "typescript": "5.9.3",
19
+ "vue-tsc": "3.3.11",
20
+ "vitest": "4.1.11",
21
+ "jsdom": "27.4.0"
22
+ }
23
+ }
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ import {computed, onMounted, onUnmounted, ref, watch} from 'vue'
3
+ import {AppRoot, ConfigProvider, Button, Callout, Form, FormActions, FormItem, SettingsSection, Textarea} from '@synopackageland/ui'
4
+ import type {AppStatus, Context, ShellPage} from '@synopackageland/sdk'
5
+ import {AppShell, BackgroundPanel, FilesPage, LoginPage, PortalSettings, createSession, shellPages, uiLocale} from '@synopackageland/sdk/vue'
6
+ import {appCopy} from './i18n'
7
+ import CapabilityPanels from './CapabilityPanels.vue'
8
+ const props = defineProps<{ctx: Context}>()
9
+ const login = createSession(props.ctx)
10
+ const copy = computed(() => appCopy(props.ctx.locale))
11
+ const app = ref<AppStatus>()
12
+ const busy = ref(false)
13
+ const error = ref('')
14
+ const notice = ref('')
15
+ const text = ref('')
16
+ let disposed = false
17
+ let unsubscribe: (() => void) | undefined
18
+ // The CLI substitutes this only for a declared package-account background worker.
19
+ const backgroundEnabled = (('__BACKGROUND_ENABLED__') as string) === 'true'
20
+ const page = ref<ShellPage>('status')
21
+ const pages = computed(() => shellPages({background:backgroundEnabled}))
22
+ const subtitle = computed(() => app.value ? `${app.value.package} · ${app.value.user} · ${app.value.version}` : undefined)
23
+ function message(e: unknown) {
24
+ const code = (e as {code?: string})?.code || 'request_failed'
25
+ const errors = copy.value.errors as Record<string, string>
26
+ return (code !== 'request_failed' && errors[code]) || errors.request_failed!.replace('{code}', code)
27
+ }
28
+ async function refresh() {
29
+ if(disposed || busy.value || (login.enabled.value && !login.subject.value)) return
30
+ busy.value = true; error.value = ''
31
+ try {
32
+ const a = await login.call<AppStatus>('app.status')
33
+ if(!disposed) app.value = a
34
+ if(a.admin) {const greeting = await login.call<{text:string}>('greeting.get'); if(!disposed) text.value = greeting.text}
35
+ } catch(e) {if(!disposed) error.value = message(e)} finally {if(!disposed) busy.value=false}
36
+ }
37
+ async function save() {
38
+ if(disposed || busy.value) return
39
+ busy.value = true; error.value='';notice.value=''
40
+ try {await login.call('greeting.set',{text:text.value});if(!disposed) notice.value=copy.value.saved}
41
+ catch(e) {if(!disposed) error.value=message(e)} finally {if(!disposed) busy.value=false}
42
+ }
43
+ watch(login.subject, (subject, previous) => {
44
+ if(!subject) {app.value=undefined;text.value='';notice.value='';error.value=''}
45
+ else if((!previous || previous.user!==subject.user)) void refresh()
46
+ })
47
+ async function signOut() {
48
+ try {await login.logout()} catch(e) {if(!disposed) error.value=message(e)}
49
+ }
50
+ onMounted(async()=>{
51
+ unsubscribe = props.ctx.reopen(()=>{if(!disposed) void refresh()})
52
+ await login.start()
53
+ if((!login.enabled.value || login.subject.value)) await refresh()
54
+ })
55
+ onUnmounted(()=>{disposed=true;unsubscribe?.();login.dispose();text.value=''})
56
+ </script>
57
+
58
+ <template>
59
+ <AppRoot modal-scope="app" style="height: 100%">
60
+ <ConfigProvider :locale="uiLocale(ctx.locale)">
61
+ <p v-if="!login.ready.value" role="status">{{ copy.checking }}</p>
62
+ <LoginPage v-else-if="login.enabled.value && !login.subject.value" :session="login" :locale="ctx.locale" />
63
+ <AppShell v-else v-model="page" :locale="ctx.locale" :pages="pages" :description="subtitle">
64
+ <template #actions><Button variant="secondary" :disabled="busy" @click="refresh">{{ copy.refresh }}</Button><Button v-if="login.enabled.value" variant="secondary" :disabled="busy" @click="signOut">{{ copy.signOut }}</Button></template>
65
+ <div class="integration-app" :aria-busy="busy" :aria-label="copy.settings">
66
+ <p v-if="busy" role="status">{{ copy.loading }}</p>
67
+ <Callout v-if="error" tone="danger" :title="copy.actionRequired"><span role="alert">{{ error }}</span></Callout>
68
+ <p v-if="notice" role="status">{{ notice }}</p>
69
+ <div data-page="status" v-show="page==='status'">
70
+ <SettingsSection :title="copy.statusTitle" :collapsible="false">
71
+ <p v-if="app">{{ app.package }} · {{ app.user }} · {{ app.version }}</p>
72
+ <p v-else-if="error" role="status">{{ copy.statusError }}</p>
73
+ </SettingsSection>
74
+ <PortalSettings v-if="ctx.portal && app" :key="app.user" :portal="ctx.portal" />
75
+ <SettingsSection v-if="app?.admin" :title="copy.greeting" :collapsible="false">
76
+ <Form @submit.prevent="save"><FormItem :label="copy.greeting"><Textarea v-model="text" :disabled="busy" /></FormItem>
77
+ <FormActions><Button variant="primary" :disabled="busy || !text.trim()" @click="save">{{ copy.save }}</Button></FormActions>
78
+ </Form>
79
+ </SettingsSection>
80
+ </div>
81
+ <CapabilityPanels v-if="app" :key="app.user" :ctx="ctx" :admin="app.admin" />
82
+ <FilesPage v-if="page==='status'" :ctx="ctx" />
83
+ <div data-page="background" v-show="page==='background'">
84
+ <BackgroundPanel v-if="backgroundEnabled && app" :key="app.user" :ctx="ctx" />
85
+ </div>
86
+ </div>
87
+ </AppShell>
88
+ </ConfigProvider>
89
+ </AppRoot>
90
+ </template>
@@ -0,0 +1,6 @@
1
+ <script setup lang="ts">
2
+ import type {Context} from '@synopackageland/sdk'
3
+ import {ExecutorSettings} from '@synopackageland/sdk/vue'
4
+ defineProps<{ctx:Context;admin:boolean}>()
5
+ </script>
6
+ <template><ExecutorSettings v-if="ctx.executor" :admin="admin" :locale="ctx.locale" /></template>
@@ -0,0 +1,20 @@
1
+ {
2
+ "checking": "Sitzung wird geprüft…",
3
+ "refresh": "Aktualisieren",
4
+ "signOut": "Abmelden",
5
+ "settings": "Paketeinstellungen",
6
+ "loading": "Paketdaten werden geladen…",
7
+ "actionRequired": "Aktion erforderlich",
8
+ "statusTitle": "Paketstatus",
9
+ "statusError": "Der Status konnte nicht geladen werden. Aktualisieren Sie die Ansicht und versuchen Sie es erneut.",
10
+ "greeting": "Begrüßung",
11
+ "save": "Speichern",
12
+ "saved": "Begrüßung gespeichert.",
13
+ "errors": {
14
+ "session_required": "Ihre DSM-Sitzung ist abgelaufen. Melden Sie sich erneut an.",
15
+ "admin_required": "Diese Aktion muss ein DSM-Administrator ausführen.",
16
+ "app_privilege_denied": "Die DSM-Anwendungsberechtigungen erlauben diesem Konto nicht, diese App zu verwenden.",
17
+ "auth_unavailable": "Die DSM-Authentifizierung ist nicht verfügbar. Aktualisieren Sie die Ansicht und versuchen Sie es erneut.",
18
+ "request_failed": "Anfrage fehlgeschlagen ({code}). Aktualisieren Sie die Ansicht und versuchen Sie es erneut."
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "checking": "Checking session…",
3
+ "refresh": "Refresh",
4
+ "signOut": "Sign out",
5
+ "settings": "Package settings",
6
+ "loading": "Loading package data…",
7
+ "actionRequired": "Action required",
8
+ "statusTitle": "Package status",
9
+ "statusError": "Status could not be loaded. Refresh to try again.",
10
+ "greeting": "Greeting",
11
+ "save": "Save",
12
+ "saved": "Greeting saved.",
13
+ "errors": {
14
+ "session_required": "Your DSM session expired. Sign in again.",
15
+ "admin_required": "A DSM administrator must perform this action.",
16
+ "app_privilege_denied": "DSM Application Privileges do not allow this account to use this app.",
17
+ "auth_unavailable": "DSM authentication is unavailable. Refresh to try again.",
18
+ "request_failed": "Request failed ({code}). Refresh and try again."
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "checking": "Comprobando la sesión…",
3
+ "refresh": "Actualizar",
4
+ "signOut": "Cerrar sesión",
5
+ "settings": "Configuración del paquete",
6
+ "loading": "Cargando datos del paquete…",
7
+ "actionRequired": "Acción necesaria",
8
+ "statusTitle": "Estado del paquete",
9
+ "statusError": "No se pudo cargar el estado. Actualice e inténtelo de nuevo.",
10
+ "greeting": "Saludo",
11
+ "save": "Guardar",
12
+ "saved": "Saludo guardado.",
13
+ "errors": {
14
+ "session_required": "Su sesión de DSM ha caducado. Vuelva a iniciar sesión.",
15
+ "admin_required": "Un administrador de DSM debe realizar esta acción.",
16
+ "app_privilege_denied": "Los privilegios de aplicación de DSM no permiten que esta cuenta use esta aplicación.",
17
+ "auth_unavailable": "La autenticación de DSM no está disponible. Actualice e inténtelo de nuevo.",
18
+ "request_failed": "La solicitud falló ({code}). Actualice e inténtelo de nuevo."
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "checking": "Vérification de la session…",
3
+ "refresh": "Actualiser",
4
+ "signOut": "Se déconnecter",
5
+ "settings": "Paramètres du paquet",
6
+ "loading": "Chargement des données du paquet…",
7
+ "actionRequired": "Action requise",
8
+ "statusTitle": "État du paquet",
9
+ "statusError": "Impossible de charger l’état. Actualisez et réessayez.",
10
+ "greeting": "Message d’accueil",
11
+ "save": "Sauvegarder",
12
+ "saved": "Message d’accueil enregistré.",
13
+ "errors": {
14
+ "session_required": "Votre session DSM a expiré. Reconnectez-vous.",
15
+ "admin_required": "Un administrateur DSM doit effectuer cette action.",
16
+ "app_privilege_denied": "Les privilèges d’application DSM n’autorisent pas ce compte à utiliser cette application.",
17
+ "auth_unavailable": "L’authentification DSM n’est pas disponible. Actualisez et réessayez.",
18
+ "request_failed": "La requête a échoué ({code}). Actualisez et réessayez."
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "checking": "Verifica della sessione…",
3
+ "refresh": "Aggiorna",
4
+ "signOut": "Disconnetti",
5
+ "settings": "Impostazioni del pacchetto",
6
+ "loading": "Caricamento dei dati del pacchetto…",
7
+ "actionRequired": "Azione richiesta",
8
+ "statusTitle": "Stato del pacchetto",
9
+ "statusError": "Impossibile caricare lo stato. Aggiornare e riprovare.",
10
+ "greeting": "Saluto",
11
+ "save": "Salva",
12
+ "saved": "Saluto salvato.",
13
+ "errors": {
14
+ "session_required": "La sessione DSM è scaduta. Accedere di nuovo.",
15
+ "admin_required": "Questa azione deve essere eseguita da un amministratore DSM.",
16
+ "app_privilege_denied": "I privilegi delle applicazioni DSM non consentono a questo account di usare questa app.",
17
+ "auth_unavailable": "L’autenticazione DSM non è disponibile. Aggiornare e riprovare.",
18
+ "request_failed": "Richiesta non riuscita ({code}). Aggiornare e riprovare."
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "checking": "セッションを確認しています…",
3
+ "refresh": "最新の情報に更新",
4
+ "signOut": "サインアウト",
5
+ "settings": "パッケージ設定",
6
+ "loading": "パッケージデータを読み込み中…",
7
+ "actionRequired": "対応が必要です",
8
+ "statusTitle": "パッケージの状態",
9
+ "statusError": "状態を読み込めませんでした。最新の情報に更新してから、もう一度お試しください。",
10
+ "greeting": "あいさつ",
11
+ "save": "保存",
12
+ "saved": "あいさつを保存しました。",
13
+ "errors": {
14
+ "session_required": "DSM セッションの有効期限が切れました。もう一度サインインしてください。",
15
+ "admin_required": "この操作は DSM 管理者が行う必要があります。",
16
+ "app_privilege_denied": "DSM のアプリケーション権限により、このアカウントはこのアプリを使用できません。",
17
+ "auth_unavailable": "DSM 認証を利用できません。最新の情報に更新してから、もう一度お試しください。",
18
+ "request_failed": "リクエストに失敗しました({code})。最新の情報に更新してから、もう一度お試しください。"
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "checking": "세션 확인 중…",
3
+ "refresh": "새로 고침",
4
+ "signOut": "로그아웃",
5
+ "settings": "패키지 설정",
6
+ "loading": "패키지 데이터를 불러오는 중…",
7
+ "actionRequired": "조치 필요",
8
+ "statusTitle": "패키지 상태",
9
+ "statusError": "상태를 불러올 수 없습니다. 새로 고친 후 다시 시도하십시오.",
10
+ "greeting": "인사말",
11
+ "save": "저장",
12
+ "saved": "인사말이 저장되었습니다.",
13
+ "errors": {
14
+ "session_required": "DSM 세션이 만료되었습니다. 다시 로그인하십시오.",
15
+ "admin_required": "이 작업은 DSM 관리자가 수행해야 합니다.",
16
+ "app_privilege_denied": "DSM 응용 프로그램 권한에서 이 계정의 앱 사용이 허용되지 않습니다.",
17
+ "auth_unavailable": "DSM 인증을 사용할 수 없습니다. 새로 고친 후 다시 시도하십시오.",
18
+ "request_failed": "요청이 실패했습니다({code}). 새로 고친 후 다시 시도하십시오."
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "checking": "正在检查登录状态…",
3
+ "refresh": "刷新",
4
+ "signOut": "注销",
5
+ "settings": "套件设置",
6
+ "loading": "正在加载套件数据…",
7
+ "actionRequired": "需要处理",
8
+ "statusTitle": "套件状态",
9
+ "statusError": "无法加载状态,请刷新后重试。",
10
+ "greeting": "问候语",
11
+ "save": "保存",
12
+ "saved": "问候语已保存。",
13
+ "errors": {
14
+ "session_required": "DSM 登录已过期,请重新登录。",
15
+ "admin_required": "此操作必须由 DSM 管理员执行。",
16
+ "app_privilege_denied": "DSM 应用程序权限不允许此帐户使用此 App。",
17
+ "auth_unavailable": "DSM 验证暂时不可用,请刷新后重试。",
18
+ "request_failed": "请求失败({code}),请刷新后重试。"
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "checking": "正在檢查登入狀態…",
3
+ "refresh": "重新整理",
4
+ "signOut": "登出",
5
+ "settings": "套件設定",
6
+ "loading": "正在載入套件資料…",
7
+ "actionRequired": "需要處理",
8
+ "statusTitle": "套件狀態",
9
+ "statusError": "無法載入狀態,請重新整理後再試。",
10
+ "greeting": "問候語",
11
+ "save": "儲存",
12
+ "saved": "問候語已儲存。",
13
+ "errors": {
14
+ "session_required": "DSM 登入已逾時,請重新登入。",
15
+ "admin_required": "此操作須由 DSM 管理員執行。",
16
+ "app_privilege_denied": "DSM 應用程式權限不允許此帳號使用這個 App。",
17
+ "auth_unavailable": "DSM 驗證目前無法使用,請重新整理後再試。",
18
+ "request_failed": "要求失敗({code}),請重新整理後再試。"
19
+ }
20
+ }
@@ -0,0 +1,13 @@
1
+ import en from './i18n/en.json'
2
+ import zhTW from './i18n/zh-TW.json'
3
+ import zhCN from './i18n/zh-CN.json'
4
+ import ja from './i18n/ja.json'
5
+ import ko from './i18n/ko.json'
6
+ import de from './i18n/de.json'
7
+ import fr from './i18n/fr.json'
8
+ import es from './i18n/es.json'
9
+ import it from './i18n/it.json'
10
+ import {uiLocale} from '@synopackageland/sdk/vue'
11
+ export type AppCopy=typeof en
12
+ export const appCatalogs:Record<string,AppCopy>={en,'zh-TW':zhTW,'zh-CN':zhCN,ja,ko,de,fr,es,it}
13
+ export function appCopy(locale:string):AppCopy{return appCatalogs[uiLocale(locale)]!}
@@ -0,0 +1,33 @@
1
+ import {createApp} from 'vue'
2
+ import App from './App.vue'
3
+ import type {Context} from '@synopackageland/sdk'
4
+ import {ExecutorSetupKey} from '@synopackageland/sdk/vue'
5
+ import '@synopackageland/ui/themes/dsm7-embedded.css'
6
+ import '@synopackageland/sdk/vue/style.css'
7
+ import './style.css'
8
+
9
+ let mountSequence = 0
10
+ export function mount(root: HTMLElement, ctx: Context) {
11
+ const app = createApp(App, {ctx})
12
+ app.provide(ExecutorSetupKey, ctx.executor ?? {
13
+ async status() {return {state: 'unsupported'}},
14
+ async authorize() {return {state: 'unsupported'}},
15
+ async control() {return {state: 'unsupported'}},
16
+ })
17
+ // Include the generated package ID, an instance counter and randomness even
18
+ // across separately loaded bundles. getRandomValues also works on DSM HTTP.
19
+ app.config.idPrefix = `__PACKAGE__-${++mountSequence}-${crypto.getRandomValues(new Uint32Array(4)).join('-')}`
20
+ const hadRootClass = root.classList.contains('dsmapp-vue-root')
21
+ root.classList.add('dsmapp-vue-root')
22
+ app.mount(root)
23
+ return {unmount() {app.unmount(); if(!hadRootClass) root.classList.remove('dsmapp-vue-root')}}
24
+ }
25
+ // Register with the existing per-package DSM window loader. Never share a
26
+ // singleton app or user/session state between different mount calls.
27
+ const script = document.currentScript as HTMLScriptElement | null
28
+ const packageID = script?.getAttribute('data-package')
29
+ const host = window as unknown as {DsmPackageApps: Record<string, {mount: typeof mount}>}
30
+ if (packageID) {
31
+ host.DsmPackageApps = host.DsmPackageApps || {}
32
+ host.DsmPackageApps[packageID] = {mount}
33
+ }
@@ -0,0 +1,27 @@
1
+ .dsmapp-vue-root {height: 100%; min-height: 0; width: 100%; overflow: hidden;}
2
+ [data-ui-app] .integration-app {padding: 0 var(--ui-page-padding-x, 16px); color: var(--ui-text); font-family: var(--ui-font-sans); font-size: 13px; line-height: 1.5;}
3
+ [data-ui-app] .integration-app section {margin-top: var(--ui-space-4, 16px);}
4
+ [data-ui-app] .integration-status h2 {font-size: 15px; font-weight: 600; margin: 0;}
5
+ [data-ui-app] .integration-status p {margin: 6px 0;}
6
+ [data-ui-app] .integration-status dl {display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin: 12px 0;}
7
+ [data-ui-app] .integration-status dt, [data-ui-app] .integration-help {color: var(--ui-text);}
8
+ [data-ui-app] .integration-status dd {margin: 0;}
9
+ [data-ui-app] .integration-actions {display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px;}
10
+ [data-ui-app] .integration-notice {color: var(--ui-text);}
11
+ [data-ui-app] .nas-summary {display: grid; grid-template-columns: minmax(6ch, max-content) minmax(0, 1fr); gap: var(--ui-space-2, 8px) var(--ui-space-4, 16px);}
12
+ [data-ui-app] .nas-summary dd {margin: 0; overflow-wrap: anywhere;}
13
+ [data-ui-app] .nas-breadcrumb {display: flex; flex-wrap: wrap; gap: var(--ui-space-2, 8px); margin-block: var(--ui-space-3, 12px);}
14
+ [data-ui-app] .nas-browser {display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--ui-space-3, 12px);}
15
+ [data-ui-app] .nas-tree {display: none;}
16
+ [data-ui-app] .nas-folder-list {min-width: 0; overflow: auto;}
17
+ [data-ui-app] .nas-folder-name {white-space: normal; overflow-wrap: anywhere; text-align: start;}
18
+ [data-ui-app] .nas-folder-action {white-space: normal; max-width: 100%;}
19
+ [data-ui-app] .nas-picker {max-height: 55vh; overflow: auto; overflow-wrap: anywhere;}
20
+ [data-ui-app] .nas-grants {list-style: none; padding: 0;}
21
+ [data-ui-app] .nas-grants > li {padding-block: var(--ui-space-4, 16px); border-bottom: 1px solid var(--ui-border);}
22
+ [data-ui-app] .nas-grant-heading {display: flex; align-items: start; justify-content: space-between; gap: var(--ui-space-3, 12px);}
23
+ [data-ui-app] .nas-grant-heading strong {overflow-wrap: anywhere;}
24
+ @media (min-width: 960px) {
25
+ [data-ui-app] .nas-browser {grid-template-columns: minmax(8ch, 1fr) minmax(0, 3fr);}
26
+ [data-ui-app] .nas-tree {display: block; overflow: auto;}
27
+ }
@@ -0,0 +1,57 @@
1
+ // @vitest-environment jsdom
2
+ import {expect,test,vi} from 'vitest'
3
+ import {nextTick} from 'vue'
4
+ import {mount} from '../src/main'
5
+ import type {Context} from '@synopackageland/sdk'
6
+ async function settle(){for(let i=0;i<16;i++){await Promise.resolve();await nextTick()}}
7
+ function fixture(id:string){
8
+ const reopen:Array<()=>void>=[]
9
+ const call=vi.fn(async(method:string)=>{
10
+ if(method==='app.status')return {package:id,version:'1',user:'admin',admin:true}
11
+ if(method==='greeting.get')return {text:'Hello '+id}
12
+ if(method==='greeting.set')return {}
13
+ throw {code:'unknown_method'}
14
+ })
15
+ const ctx={locale:'en',call,reopen(fn:()=>void){reopen.push(fn);return ()=>{}}} as Context
16
+ const root=document.createElement('div');document.body.append(root)
17
+ const app=mount(root,ctx)
18
+ return {root,app,call,reopen,ctx,close(){app.unmount();root.remove()}}
19
+ }
20
+ test('independent mounts load and save their own package greeting',async()=>{
21
+ const a=fixture('Alpha'),b=fixture('Beta')
22
+ try{
23
+ await settle()
24
+ expect(a.root.textContent).toContain('Alpha');expect(a.root.textContent).not.toContain('Beta')
25
+ expect((a.root.querySelector('textarea') as HTMLTextAreaElement).value).toBe('Hello Alpha')
26
+ ;[...a.root.querySelectorAll('button')].find(n=>n.textContent==='Save')!.click();await settle()
27
+ expect(a.call).toHaveBeenCalledWith('greeting.set',{text:'Hello Alpha'})
28
+ expect(b.call.mock.calls.some(([method])=>method==='greeting.set')).toBe(false)
29
+ const count=b.call.mock.calls.length;a.reopen.forEach(fn=>fn());await settle()
30
+ expect(b.call.mock.calls).toHaveLength(count)
31
+ a.app.unmount();const stopped=a.call.mock.calls.length;a.reopen.forEach(fn=>fn());await settle()
32
+ expect(a.call.mock.calls).toHaveLength(stopped)
33
+ }finally{a.close();b.close()}
34
+ })
35
+ test('failed status loading offers a retry and releases the mounting class',async()=>{
36
+ const f=fixture('Failure')
37
+ try{
38
+ await settle();f.call.mockRejectedValue({code:'auth_unavailable'});f.reopen.forEach(fn=>fn());await settle()
39
+ expect(f.root.textContent).toContain('DSM authentication is unavailable')
40
+ expect(f.root.querySelector('button')?.disabled).toBe(false)
41
+ f.app.unmount();expect(f.root.classList.contains('dsmapp-vue-root')).toBe(false)
42
+ }finally{f.close()}
43
+ })
44
+ test('a portal app shows the Application Portal section on the status page',async()=>{
45
+ const without=fixture('Plain')
46
+ const f=fixture('Portal')
47
+ try{
48
+ await settle();expect(without.root.textContent).not.toContain('Application Portal')
49
+ f.app.unmount()
50
+ const status={state:'active',alias:'portal',url:'https://nas.example/portal/',canManage:false,controlPanel:false,defaultAlias:'portal'} as const
51
+ f.ctx.portal={messages:{title:'Application Portal',link:'Direct link:'},status:vi.fn(async()=>status),apply:vi.fn(),disable:vi.fn(),openControlPanel:vi.fn(),mountPanel:vi.fn()}
52
+ const app=mount(f.root,f.ctx);await settle()
53
+ expect(f.root.textContent).toContain('Application Portal')
54
+ expect(f.root.querySelector('a[href="https://nas.example/portal/"]')).not.toBeNull()
55
+ app.unmount()
56
+ }finally{without.close();f.root.remove()}
57
+ })
@@ -0,0 +1,27 @@
1
+ import {readdirSync,readFileSync} from 'node:fs'
2
+ import {join} from 'node:path'
3
+ import {expect,test} from 'vitest'
4
+ const root=join(__dirname,'../src')
5
+ const sources=(dir:string):string[]=>readdirSync(dir,{withFileTypes:true}).flatMap(e=>e.isDirectory()?sources(join(dir,e.name)):e.name.endsWith('.vue')?[join(dir,e.name)]:[])
6
+ const template=(file:string)=>readFileSync(file,'utf8').replace(/<script[\s\S]*?<\/script>/g,'').replace(/<style[\s\S]*?<\/style>/g,'')
7
+ const uses=(file:string,tag:string)=>new RegExp(`<${tag}[\\s>/]`).test(template(file))
8
+ // The template ships @synopackageland/ui controls only; a native form control
9
+ // means a missing UI component, which is a UI gap to report, not to paper over.
10
+ test.each(['select','option','textarea','label','input','a'])('no vue template renders a native <%s>',tag=>{
11
+ expect(sources(root).filter(file=>uses(file,tag))).toEqual([])
12
+ })
13
+ // Form direction and the control id come from the UI: `direction="auto"` stacks labels in a
14
+ // narrow package window, and a control inside a FormItem binds its id/ARIA by itself.
15
+ test('FormItem controls rely on the UI auto layout and id binding',()=>{
16
+ const items=sources(root).filter(file=>uses(file,'FormItem'))
17
+ expect(items.length).toBeGreaterThan(0)
18
+ expect(items.filter(file=>/direction="vertical"|field\.controlId|#default="\{field\}"/.test(template(file)))).toEqual([])
19
+ })
20
+ // UI built-in text follows the Provider locale (nine locales), so every app root sets it once and no
21
+ // component repeats a per-control locale or download label.
22
+ test('app roots set the UI locale once and components do not translate UI text themselves',()=>{
23
+ const roots=sources(root).filter(file=>uses(file,'AppRoot'))
24
+ expect(roots.length).toBeGreaterThan(0)
25
+ expect(roots.filter(file=>!uses(file,'ConfigProvider'))).toEqual([])
26
+ expect(sources(root).filter(file=>/download-label|external-label|<FilePicker[^>]*:locale=/.test(template(file)))).toEqual([])
27
+ })
@@ -0,0 +1,50 @@
1
+ // @vitest-environment jsdom
2
+ // @vitest-environment-options {"url":"https://nas.test"}
3
+ import {expect,test,vi} from 'vitest'
4
+ import {nextTick} from 'vue'
5
+ import {mount} from '../src/main'
6
+ import type {Context} from '@synopackageland/sdk'
7
+ import {appCopy} from '../src/i18n'
8
+ import {identityCopy} from '@synopackageland/sdk/vue'
9
+ async function settle(){for(let i=0;i<20;i++){await Promise.resolve();await nextTick()}}
10
+ const buttons=(root:HTMLElement)=>[...root.querySelectorAll('button')].map(b=>b.textContent?.trim())
11
+ function start(locale:string,call:(method:string)=>Promise<unknown>){
12
+ const ctx={locale,call:vi.fn(call),reopen(){return ()=>{}}} as unknown as Context
13
+ const root=document.createElement('div');document.body.append(root)
14
+ const app=mount(root,ctx)
15
+ return {root,close(){app.unmount();root.remove()}}
16
+ }
17
+ test('the status page and its errors follow the DSM language',async()=>{
18
+ const copy=appCopy('ger')
19
+ const {root,close}=start('de',async method=>{
20
+ if(method==='app.status')return {package:'Pkg',version:'1',user:'admin',admin:true}
21
+ if(method==='greeting.get')throw {code:'admin_required'}
22
+ throw {code:'unknown_method'}
23
+ })
24
+ try{
25
+ await settle()
26
+ expect(buttons(root)).toContain(copy.refresh)
27
+ expect(root.textContent).toContain(copy.statusTitle)
28
+ expect(root.textContent).toContain(copy.greeting)
29
+ expect(root.textContent).toContain(copy.errors.admin_required)
30
+ expect(root.querySelector('[aria-label]')?.getAttribute('aria-label')).not.toBe('Package settings')
31
+ }finally{close()}
32
+ })
33
+ test('unknown failures name the code in the DSM language',()=>{
34
+ expect(appCopy('ja').errors.request_failed).toContain('{code}')
35
+ })
36
+ test('the sign-in page and its session notices follow the DSM language',async()=>{
37
+ const copy=identityCopy('jpn')
38
+ const {root,close}=start('ja',async method=>{
39
+ if(method==='identity.status')return {enabled:true}
40
+ if(method==='identity.session')return {user:'alice',uid:'1',expiresAt:new Date(Date.now()-1000).toISOString()}
41
+ throw {code:'session_required'}
42
+ })
43
+ try{
44
+ await settle()
45
+ expect(root.textContent).toContain(copy.title)
46
+ expect(buttons(root)).toEqual(expect.arrayContaining([copy.signIn,copy.continueDSM]))
47
+ expect(root.querySelector('[role="status"]')?.textContent).toBe(copy.notices.expired)
48
+ expect(root.textContent).not.toMatch(/Sign in|Account|Password|session/)
49
+ }finally{close()}
50
+ })
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"ES2022","module":"ESNext","moduleResolution":"Bundler","resolveJsonModule":true,"strict":true,"lib":["ES2022","DOM"],"skipLibCheck":true,"types":["vite/client"]},"include":["src/**/*.ts","src/**/*.vue","vite.config.ts"]}
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ // @ts-expect-error The shared notice tool is a plain Node module.
4
+ import { writeThirdPartyNotices } from './third-party-notices.mjs'
5
+ export default defineConfig(({command}) => ({
6
+ plugins: [vue(), {name: 'dependency-notices', closeBundle() {writeThirdPartyNotices('.', 'dist/LICENSES', {ui: true})}}],
7
+ // Library mode must replace Vue dependency env guards for the DSM browser.
8
+ define: command === 'build' ? {'process.env.NODE_ENV': JSON.stringify('production')} : {},
9
+ resolve: {dedupe: ['vue']},
10
+ build: {lib: {entry: 'src/main.ts', formats: ['iife'], name: 'DsmVueApp', fileName: () => 'app.js'}, cssCodeSplit: false, rollupOptions: {output: {assetFileNames: 'app.[ext]'}}},
11
+ }))
@@ -0,0 +1,2 @@
1
+ dist/
2
+ node_modules/