pull/24694/merge
Rahul Kumar Saini 2025-12-18 21:11:00 -06:00 committed by GitHub
commit 8773b03d29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 82 additions and 8 deletions

View File

@ -309,6 +309,10 @@
"storage_template_enable_description": "Enable storage template engine",
"storage_template_hash_verification_enabled": "Hash verification enabled",
"storage_template_hash_verification_enabled_description": "Enables hash verification, don't disable this unless you're certain of the implications",
"folder_content_order": "Folder content order",
"folder_content_order_description": "Set the order in which assets are displayed in the folder view",
"folder_content_order_name": "File name",
"folder_content_order_date": "Creation date",
"storage_template_migration": "Storage template migration",
"storage_template_migration_description": "Apply the current <link>{template}</link> to previously uploaded assets",
"storage_template_migration_info": "The storage template will convert all extensions to lowercase. Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the <link>{job}</link>.",

View File

@ -16554,6 +16554,13 @@
],
"type": "object"
},
"FolderContentOrder": {
"enum": [
"name",
"date"
],
"type": "string"
},
"FoldersResponse": {
"properties": {
"enabled": {
@ -21972,6 +21979,13 @@
"enabled": {
"type": "boolean"
},
"folderContentOrder": {
"allOf": [
{
"$ref": "#/components/schemas/FolderContentOrder"
}
]
},
"hashVerificationEnabled": {
"type": "boolean"
},
@ -21981,6 +21995,7 @@
},
"required": [
"enabled",
"folderContentOrder",
"hashVerificationEnabled",
"template"
],

View File

@ -1581,6 +1581,7 @@ export type SystemConfigServerDto = {
};
export type SystemConfigStorageTemplateDto = {
enabled: boolean;
folderContentOrder: FolderContentOrder;
hashVerificationEnabled: boolean;
template: string;
};
@ -5639,6 +5640,10 @@ export enum OAuthTokenEndpointAuthMethod {
ClientSecretPost = "client_secret_post",
ClientSecretBasic = "client_secret_basic"
}
export enum FolderContentOrder {
Name = "name",
Date = "date"
}
export enum TriggerType {
AssetCreate = "AssetCreate",
PersonRecognized = "PersonRecognized"

View File

@ -3,6 +3,7 @@ import {
AudioCodec,
Colorspace,
CQMode,
FolderContentOrder,
ImageFormat,
LogLevel,
OAuthTokenEndpointAuthMethod,
@ -121,6 +122,7 @@ export interface SystemConfig {
storageTemplate: {
enabled: boolean;
hashVerificationEnabled: boolean;
folderContentOrder: FolderContentOrder;
template: string;
};
image: {
@ -311,6 +313,7 @@ export const defaults = Object.freeze<SystemConfig>({
storageTemplate: {
enabled: false,
hashVerificationEnabled: true,
folderContentOrder: FolderContentOrder.Name,
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
},
image: {

View File

@ -20,6 +20,7 @@ import {
AudioCodec,
CQMode,
Colorspace,
FolderContentOrder,
ImageFormat,
LogLevel,
OAuthTokenEndpointAuthMethod,
@ -545,6 +546,9 @@ class SystemConfigStorageTemplateDto {
@IsNotEmpty()
@IsString()
template!: string;
@ValidateEnum({ enum: FolderContentOrder, name: 'FolderContentOrder' })
folderContentOrder!: FolderContentOrder;
}
export class SystemConfigTemplateStorageOptionDto {

View File

@ -873,3 +873,8 @@ export enum PluginTriggerType {
AssetCreate = 'AssetCreate',
PersonRecognized = 'PersonRecognized',
}
export enum FolderContentOrder {
Name = 'name',
Date = 'date',
}

View File

@ -1,7 +1,7 @@
import { Kysely } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetVisibility } from 'src/enum';
import { AssetVisibility, FolderContentOrder } from 'src/enum';
import { DB } from 'src/schema';
import { asUuid, withExif } from 'src/utils/database';
@ -26,8 +26,16 @@ export class ViewRepository {
return results.map((row) => row.directoryPath.replaceAll(/\/$/g, ''));
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
async getAssetsByOriginalPath(userId: string, partialPath: string) {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, FolderContentOrder.Name] })
@GenerateSql({
params: [DummyValue.UUID, DummyValue.STRING, FolderContentOrder.Date],
name: 'getAssetsByOriginalPath (date order)',
})
async getAssetsByOriginalPath(
userId: string,
partialPath: string,
order: FolderContentOrder = FolderContentOrder.Name,
) {
const normalizedPath = partialPath.replaceAll(/\/$/g, '');
return this.db
@ -42,10 +50,13 @@ export class ViewRepository {
.where('localDateTime', 'is not', null)
.where('originalPath', 'like', `%${normalizedPath}/%`)
.where('originalPath', 'not like', `%${normalizedPath}/%/%`)
.orderBy(
.$if(order === FolderContentOrder.Name, (qb) =>
qb.orderBy(
(eb) => eb.fn('regexp_replace', ['asset.originalPath', eb.val('.*/(.+)'), eb.val(String.raw`\1`)]),
'asc',
),
)
.$if(order === FolderContentOrder.Date, (qb) => qb.orderBy('fileCreatedAt', 'asc'))
.execute();
}
}

View File

@ -4,6 +4,7 @@ import {
AudioCodec,
Colorspace,
CQMode,
FolderContentOrder,
ImageFormat,
LogLevel,
OAuthTokenEndpointAuthMethod,
@ -159,6 +160,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
storageTemplate: {
enabled: false,
hashVerificationEnabled: true,
folderContentOrder: FolderContentOrder.Name,
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
},
image: {

View File

@ -1,4 +1,5 @@
import { mapAsset } from 'src/dtos/asset-response.dto';
import { FolderContentOrder } from 'src/enum';
import { ViewService } from 'src/services/view.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
@ -44,6 +45,11 @@ describe(ViewService.name, () => {
const result = await sut.getAssetsByOriginalPath(authStub.admin, path);
expect(result).toEqual(mockAssetReponseDto);
await expect(mocks.view.getAssetsByOriginalPath(authStub.admin.user.id, path)).resolves.toEqual(mockAssets);
expect(mocks.view.getAssetsByOriginalPath).toHaveBeenCalledWith(
authStub.admin.user.id,
path,
FolderContentOrder.Name,
);
});
});
});

View File

@ -10,7 +10,12 @@ export class ViewService extends BaseService {
}
async getAssetsByOriginalPath(auth: AuthDto, path: string): Promise<AssetResponseDto[]> {
const assets = await this.viewRepository.getAssetsByOriginalPath(auth.user.id, path);
const { storageTemplate } = await this.getConfig({ withCache: true });
const assets = await this.viewRepository.getAssetsByOriginalPath(
auth.user.id,
path,
storageTemplate.folderContentOrder,
);
return assets.map((asset) => mapAsset(asset, { auth }));
}
}

View File

@ -4,6 +4,7 @@
import SupportedVariablesPanel from '$lib/components/admin-settings/SupportedVariablesPanel.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { AppRoute, SettingInputFieldType } from '$lib/constants';
import FormatMessage from '$lib/elements/FormatMessage.svelte';
@ -11,7 +12,7 @@
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
import { handleSystemConfigSave } from '$lib/services/system-config.service';
import { user } from '$lib/stores/user.store';
import { getStorageTemplateOptions, type SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
import { FolderContentOrder, getStorageTemplateOptions, type SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
import { LoadingSpinner } from '@immich/ui';
import handlebar from 'handlebars';
import * as luxon from 'luxon';
@ -150,6 +151,19 @@
configToEdit.storageTemplate.hashVerificationEnabled === config.storageTemplate.hashVerificationEnabled
)}
/>
<SettingSelect
label={$t('admin.folder_content_order')}
desc={$t('admin.folder_content_order_description')}
bind:value={configToEdit.storageTemplate.folderContentOrder}
options={[
{ value: FolderContentOrder.Name, text: $t('admin.folder_content_order_name') },
{ value: FolderContentOrder.Date, text: $t('admin.folder_content_order_date') },
]}
isEdited={configToEdit.storageTemplate.folderContentOrder !== config.storageTemplate.folderContentOrder}
{disabled}
name="folder-content-order"
/>
{/if}
{#if configToEdit.storageTemplate.enabled}